1#![warn(missing_docs)]
20
21use crate::usb::UsbBackend;
22use anyhow::Context;
23use clap::Parser;
24use clap::ValueEnum;
25use cxl_spec::CfmwsWindowRestrictions;
26use guid::Guid;
27use net_backend_resources::mac_address::MacAddress;
28use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
29use openvmm_defs::config::DeviceVtl;
30use openvmm_defs::config::PcatBootDevice;
31use openvmm_defs::config::Vtl2BaseAddressType;
32use openvmm_defs::config::X2ApicConfig;
33use std::ffi::OsString;
34use std::net::SocketAddr;
35use std::path::PathBuf;
36use std::str::FromStr;
37use thiserror::Error;
38
39pub(crate) fn parse_options() -> Options {
43 fn on_big_stack<R: Send>(f: impl Send + FnOnce() -> R) -> R {
53 if cfg!(windows) {
54 std::thread::scope(|s| {
55 std::thread::Builder::new()
56 .stack_size(0x400000)
57 .spawn_scoped(s, f)
58 .unwrap()
59 .join()
60 .unwrap()
61 })
62 } else {
63 f()
64 }
65 }
66
67 on_big_stack(Options::parse)
68}
69
70const DEFAULT_MEMORY_SIZE: u64 = 1024 * 1024 * 1024;
71
72#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
79pub struct MemoryCli {
80 pub size: Option<vmm_cli::MemorySize>,
82 #[kv(flag)]
85 pub shared: Option<bool>,
86 #[kv(flag)]
88 pub prefetch: bool,
89 #[kv(flag, key = "thp")]
92 pub transparent_hugepages: Option<bool>,
93 #[kv(flag)]
95 pub hugepages: bool,
96 pub hugepage_size: Option<vmm_cli::MemorySize>,
98 pub file: Option<PathBuf>,
100}
101
102impl MemoryCli {
103 pub fn validate(&self) -> anyhow::Result<()> {
105 if self.hugepage_size.is_some() && !self.hugepages {
106 anyhow::bail!("hugepage_size requires hugepages=on");
107 }
108 if self.hugepages {
109 if self.shared == Some(false) {
110 anyhow::bail!("hugepages=on conflicts with shared=off");
111 }
112 if self.file.is_some() {
113 anyhow::bail!("hugepages=on conflicts with file=...");
114 }
115 }
116 Ok(())
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, vmm_cli::KeyValueArgs)]
122pub struct NumaNodeCli {
123 #[kv(flatten)]
125 pub memory: MemoryCli,
126 pub host_numa_node: Option<u32>,
128 pub vps: Option<vmm_cli::BracketRangeList>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct NumaDistanceCli {
135 pub src: u32,
137 pub dst: u32,
139 pub distance: u8,
141}
142
143#[derive(Parser)]
148#[command(
149 name = "openvmm",
150 version = openvmm_build_info::get().version(),
151 long_version = openvmm_build_info::get().long_version(),
152)]
153pub struct Options {
154 #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
156 pub processors: u32,
157
158 #[clap(
160 short = 'm',
161 long,
162 value_name = "PARAMS",
163 default_value = "1GB",
164 value_parser = parse_memory_config,
165 conflicts_with = "numa",
166 long_help = r#"Configure guest RAM.
167
168Syntax: SIZE | key=value[,key=value...]
169
170Size suffixes accept K, M, G, and T, optionally followed by B.
171
172Options:
173 size=<SIZE> guest RAM size, default 1GB
174 shared[=on|off] use shared file-backed RAM, default on
175 prefetch[=on|off] pre-populate guest RAM mappings
176 thp[=on|off] mark guest RAM as THP-eligible (Linux), default on
177 hugepages[=on|off] allocate RAM from hugetlb/large pages (Linux, Windows)
178 hugepage_size=<SIZE> hugepage size, default 2MB; requires hugepages=on
179 file=<PATH> use an existing file as guest RAM backing
180
181Examples:
182 --memory 4G
183 --memory size=64GB,hugepages=on,hugepage_size=2MB
184 --memory size=4G,file=path/to/memory.bin
185 --memory size=4G,thp=off"#
186 )]
187 pub memory: MemoryCli,
188
189 #[clap(
194 long,
195 value_name = "PARAMS",
196 value_parser = parse_numa_node,
197 conflicts_with = "memory",
198 long_help = r#"Configure a guest NUMA node (repeatable, one per node).
199
200Syntax: key=value[,key=value...]
201
202Options:
203 size=<SIZE> RAM for this node (required)
204 shared[=on|off] use shared file-backed RAM, default on
205 prefetch[=on|off] pre-populate guest RAM mappings
206 thp[=on|off] mark node RAM as THP-eligible (Linux), default on
207 hugepages[=on|off] allocate RAM from hugetlb/large pages (Linux, Windows)
208 hugepage_size=<SIZE> hugepage size, default 2MB; requires hugepages=on
209 host_numa_node=<N> bind allocation to host NUMA node N
210 vps=<LIST> explicit VP indices (e.g. "[0,1,2,3]")
211
212 VP lists use bracket syntax with comma-separated indices and dash
213 ranges: vps=[0,1] or vps=[0-3] or vps=[0,1,4-5]. An empty list, vps=[],
214 declares a CPU-less node (e.g. a generic-initiator target); unlike a
215 non-empty list, it may be combined with nodes that omit vps.
216
217Examples:
218 --numa size=2G --numa size=2G
219 --numa size=2G,host_numa_node=0 --numa size=2G,host_numa_node=1
220 --numa size=2G,hugepages=on,vps=[0,1] --numa size=2G,vps=[2,3]
221 --numa size=2G,vps=[0-3] --numa size=2G,vps=[4-7]
222 --numa size=2G --numa size=0,vps=[]"#
223 )]
224 pub numa: Option<Vec<NumaNodeCli>>,
225
226 #[clap(long, value_name = "SRC:DST:DIST", value_parser = parse_numa_distance, conflicts_with = "memory", requires = "numa")]
231 pub numa_distance: Option<Vec<NumaDistanceCli>>,
232
233 #[clap(short = 'M', long, hide = true)]
235 pub shared_memory: bool,
236
237 #[clap(long = "prefetch", hide = true, conflicts_with = "numa")]
239 pub deprecated_prefetch: bool,
240
241 #[clap(
245 long = "memory-backing-file",
246 value_name = "FILE",
247 hide = true,
248 conflicts_with_all = ["deprecated_private_memory", "numa"]
249 )]
250 pub deprecated_memory_backing_file: Option<PathBuf>,
251
252 #[clap(
256 long,
257 value_name = "DIR",
258 conflicts_with_all = ["deprecated_memory_backing_file", "numa"]
259 )]
260 pub restore_snapshot: Option<PathBuf>,
261
262 #[clap(long = "private-memory", hide = true, conflicts_with_all = ["deprecated_memory_backing_file", "restore_snapshot", "numa"])]
264 pub deprecated_private_memory: bool,
265
266 #[clap(long = "thp", hide = true, conflicts_with = "numa")]
268 pub deprecated_thp: bool,
269
270 #[clap(short = 'P', long)]
272 pub paused: bool,
273
274 #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
276 pub kernel: OptionalPathBuf,
277
278 #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
280 pub initrd: OptionalPathBuf,
281
282 #[clap(short = 'c', long, value_name = "STRING")]
284 pub cmdline: Vec<String>,
285
286 #[clap(long)]
288 pub hv: bool,
289
290 #[clap(
293 long,
294 requires_all = ["uefi", "no_vmbus"],
295 conflicts_with_all = ["hv", "vtl2", "get", "pcat", "igvm"]
296 )]
297 pub no_hv: bool,
298
299 #[clap(long, conflicts_with_all = ["uefi", "pcat", "igvm", "smbios"])]
303 pub device_tree: bool,
304
305 #[clap(
307 long,
308 value_name = "PARAMS",
309 value_parser = parse_smbios,
310 long_help = r#"Override the guest's SMBIOS (DMI) identity.
311
312Syntax: type=N,key=value[,key=value...]
313
314`type` selects the SMBIOS structure and is required; there is no default.
315Unset keys use the loader's built-in default identity. Repeat --smbios to set
316fields across multiple structure types.
317
318OpenVMM Linux direct boot supports all listed fields. OpenHCL Linux direct
319boot and UEFI boot support Type 1 fields only. PCAT boot supports only Type 1
320`serial` and `uuid`. Unsupported fields are rejected with an error.
321
322Type 0 (BIOS Information):
323 vendor=<STRING> BIOS vendor
324 version=<STRING> BIOS version
325 date=<STRING> BIOS release date
326 release=<MAJOR.MINOR> System BIOS Major/Minor Release (e.g. 4.1)
327
328Type 1 (System Information):
329 manufacturer=<STRING> system manufacturer (sys_vendor)
330 product=<STRING> product name (product_name)
331 version=<STRING> product version (product_version)
332 serial=<STRING> serial number (product_serial)
333 uuid=<GUID|random> system UUID (product_uuid); `random` generates a
334 fresh per-VM GUID. Unset => all-zero GUID.
335 sku=<STRING> SKU number (product_sku)
336 family=<STRING> product family (product_family)
337
338Examples:
339 --smbios type=1,manufacturer=Contoso,product="Virtual Machine"
340 --smbios type=1,uuid=12345678-9abc-def0-1234-56789abcdef0
341 --smbios type=1,uuid=random
342 --smbios type=0,vendor=Contoso,version=1.0"#
343 )]
344 pub smbios: Vec<SmbiosCli>,
345
346 #[clap(long, requires("hv"))]
350 pub vtl2: bool,
351
352 #[clap(long, requires("hv"))]
355 pub get: bool,
356
357 #[clap(long, conflicts_with("get"))]
360 pub no_get: bool,
361
362 #[clap(
364 long,
365 conflicts_with_all = [
366 "vmbus_vsock_path",
367 "vmbus_vtl2_vsock_path",
368 "vmbus_redirect",
369 "vmbus_max_version",
370 "vmbus_com1_serial",
371 "vmbus_com2_serial",
372 "vtl2",
373 "get",
374 "pcat",
375 ],
376 )]
377 pub no_vmbus: bool,
378
379 #[clap(long, requires("vtl2"))]
381 pub no_alias_map: bool,
382
383 #[clap(long)]
385 pub isolation: Option<IsolationCli>,
386
387 #[clap(long)]
389 pub snp_restricted_injection: bool,
390
391 #[clap(long, value_name = "PATH", alias = "vsock-path")]
393 pub vmbus_vsock_path: Option<String>,
394
395 #[clap(long, value_name = "PATH", requires("vtl2"), alias = "vtl2-vsock-path")]
397 pub vmbus_vtl2_vsock_path: Option<String>,
398
399 #[clap(long, requires("vtl2"), default_value = "halt")]
401 pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
402
403 #[clap(long_help = r#"
405e.g: --disk memdiff:file:/path/to/disk.vhd
406
407syntax: <path> | kind:<arg>[,flag,opt=arg,...]
408
409valid disk kinds:
410 `mem:<len>` memory backed disk
411 <len>: length of ramdisk, e.g.: `1G`
412 `memdiff:<disk>` memory backed diff disk
413 <disk>: lower disk, e.g.: `file:base.img`
414 `file:<path>[;direct][;create=<len>]` file-backed disk
415 <path>: path to file
416 `;direct`: bypass the OS page cache
417 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
418 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
419 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
420 `blob:<type>:<url>` HTTP blob (read-only)
421 <type>: `flat` or `vhd1`
422 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
423 <cipher>: `xts-aes-256`
424 `prwrap:<disk>` persistent reservations wrapper
425
426flags:
427 `ro` open disk as read-only
428 `dvd` specifies that device is cd/dvd and it is read_only
429 `vtl2` assign this disk to VTL2
430 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
431 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
432
433options:
434 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
435 `on=<name>` attach to a named controller (NVMe or SCSI), incompatible with `pcie_port` and `vtl2`
436 `nsid=<N>` NVMe namespace ID (1-based), requires `on`; auto-assigned if omitted
437 `lun=<N>` SCSI LUN (0-based), requires `on`; auto-assigned if omitted
438 `relay=<ctrl>[:<loc>]` relay through OpenHCL to the named OpenHCL controller, with optional location (LUN or NSID)
439"#)]
440 #[clap(long, value_name = "FILE")]
441 pub disk: Vec<DiskCli>,
442
443 #[clap(long_help = r#"
447e.g: --nvme memdiff:file:/path/to/disk.vhd
448
449syntax: <path> | kind:<arg>[,flag,opt=arg,...]
450
451valid disk kinds:
452 `mem:<len>` memory backed disk
453 <len>: length of ramdisk, e.g.: `1G`
454 `memdiff:<disk>` memory backed diff disk
455 <disk>: lower disk, e.g.: `file:base.img`
456 `file:<path>[;direct][;create=<len>]` file-backed disk
457 <path>: path to file
458 `;direct`: bypass the OS page cache
459 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
460 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
461 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
462 `blob:<type>:<url>` HTTP blob (read-only)
463 <type>: `flat` or `vhd1`
464 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
465 <cipher>: `xts-aes-256`
466 `prwrap:<disk>` persistent reservations wrapper
467
468flags:
469 `ro` open disk as read-only
470 `vtl2` assign this disk to VTL2
471 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
472 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
473
474options:
475 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
476"#)]
477 #[clap(long)]
478 pub nvme: Vec<DiskCli>,
479
480 #[clap(long_help = r#"
482Create a named NVMe controller with an explicit transport.
483
484syntax: id=<name>,pcie_port=<port> | id=<name>,vpci[=<guid>]
485
486The controller name can be referenced by `--disk` with the `on=<name>`
487option to attach namespaces to this controller.
488
489options:
490 `id=<name>` controller name (required)
491 `pcie_port=<port>` present on PCIe under the specified port
492 `vpci[=<guid>]` present via VPCI; optional instance GUID
493 `vtl2` assign to VTL2 (default VTL0)
494
495Exactly one of `pcie_port` or `vpci` must be specified.
496
497Examples:
498 --nvme-pci id=nvme0,pcie_port=p0
499 --nvme-pci id=nvme1,vpci
500 --nvme-pci id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c
501"#)]
502 #[clap(long = "nvme-pci")]
503 pub nvme_pci: Vec<NvmeControllerCli>,
504
505 #[clap(long_help = r#"
507Create a named VMBus SCSI controller.
508
509syntax: id=<name>[,sub_channels=<N>][,vtl2]
510
511The controller name can be referenced by `--disk` with the `on=<name>`
512option to attach disks to this controller.
513
514options:
515 `id=<name>` controller name (required)
516 `sub_channels=<N>` number of sub-channels (default 0)
517 `vtl2` assign to VTL2 (default VTL0)
518
519Examples:
520 --vmbus-scsi id=scsi0
521 --vmbus-scsi id=scsi1,sub_channels=4
522"#)]
523 #[clap(long = "vmbus-scsi")]
524 pub vmbus_scsi: Vec<ScsiControllerCli>,
525
526 #[clap(long_help = r#"
528Register an OpenHCL-managed storage controller that can be used as a
529relay target with `--disk ... relay=<name>`.
530
531syntax: id=<name>,type=scsi|nvme[,guid=<guid>]
532
533options:
534 `id=<name>` controller name (required)
535 `type=scsi|nvme` controller protocol (required)
536 `guid=<guid>` instance GUID (auto-derived from name if omitted)
537
538Examples:
539 --openhcl-controller id=vtl0-scsi,type=scsi
540 --openhcl-controller id=vtl0-nvme,type=nvme,guid=09a59b81-...
541"#)]
542 #[clap(long = "openhcl-controller")]
543 pub openhcl_controller: Vec<OpenhclControllerCli>,
544
545 #[clap(long = "cxl-test", value_name = "mem:<len>,pcie_port=<name>")]
547 pub cxl_test: Vec<CxlTestDeviceCli>,
548
549 #[clap(long_help = r#"
551e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
552
553syntax: <path> | kind:<arg>[,flag,opt=arg,...]
554
555valid disk kinds:
556 `mem:<len>` memory backed disk
557 <len>: length of ramdisk, e.g.: `1G`
558 `memdiff:<disk>` memory backed diff disk
559 <disk>: lower disk, e.g.: `file:base.img`
560 `file:<path>[;direct]` file-backed disk
561 <path>: path to file
562 `;direct`: bypass the OS page cache
563
564flags:
565 `ro` open disk as read-only
566
567options:
568 `pcie_port=<name>` present the disk using pcie under the specified port
569 `serial=<value>` 1-20 printable ASCII bytes except comma and brackets
570"#)]
571 #[clap(long = "virtio-blk")]
572 pub virtio_blk: Vec<DiskCli>,
573
574 #[cfg(target_os = "linux")]
599 #[clap(long = "vhost-user")]
600 pub vhost_user: Vec<VhostUserCli>,
601
602 #[clap(long, value_name = "COUNT", default_value = "0")]
604 pub scsi_sub_channels: u16,
605
606 #[clap(long)]
608 pub nic: bool,
609
610 #[clap(long)]
626 pub net: Vec<NicConfigCli>,
627
628 #[clap(long, value_name = "SWITCH_ID")]
632 pub kernel_vmnic: Vec<String>,
633
634 #[clap(long)]
636 pub gfx: bool,
637
638 #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
640 pub vtl2_gfx: bool,
641
642 #[clap(flatten)]
644 pub vnc: VncCli,
645
646 #[cfg(guest_arch = "x86_64")]
648 #[clap(long, default_value_t)]
649 pub apic_id_offset: u32,
650
651 #[clap(long)]
653 pub vps_per_socket: Option<u32>,
654
655 #[clap(long, default_value = "auto")]
657 pub smt: SmtConfigCli,
658
659 #[cfg(guest_arch = "x86_64")]
661 #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
662 pub x2apic: X2ApicConfig,
663
664 #[cfg(guest_arch = "aarch64")]
666 #[clap(long, default_value = "auto")]
667 pub gic_msi: GicMsiCli,
668
669 #[cfg(guest_arch = "aarch64")]
673 #[clap(long, value_name = "SMMU_CONFIG")]
674 pub smmu: Vec<SmmuCli>,
675
676 #[clap(long, value_name = "SERIAL")]
688 pub com1: Option<ComSerialConfigCli>,
689
690 #[clap(long, value_name = "SERIAL")]
693 pub com2: Option<ComSerialConfigCli>,
694
695 #[clap(long, value_name = "SERIAL")]
698 pub com3: Option<ComSerialConfigCli>,
699
700 #[clap(long, value_name = "SERIAL")]
703 pub com4: Option<ComSerialConfigCli>,
704
705 #[structopt(long, value_name = "SERIAL")]
707 pub vmbus_com1_serial: Option<SerialConfigCli>,
708
709 #[structopt(long, value_name = "SERIAL")]
711 pub vmbus_com2_serial: Option<SerialConfigCli>,
712
713 #[clap(long)]
715 pub serial_tx_only: bool,
716
717 #[clap(long, value_name = "SERIAL")]
719 pub debugcon: Option<DebugconSerialConfigCli>,
720
721 #[clap(
723 long,
724 short = 'e',
725 value_name = "OPTIONS",
726 num_args = 0..=1,
727 default_missing_value = ""
728 )]
729 pub uefi: Option<UefiCli>,
730
731 #[clap(
733 long = "uefi-firmware",
734 hide = true,
735 requires("uefi"),
736 conflicts_with("igvm"),
737 value_name = "FILE"
738 )]
739 pub deprecated_uefi_firmware: Option<PathBuf>,
740
741 #[clap(long = "uefi-debug", hide = true, requires("uefi"))]
743 pub deprecated_uefi_debug: bool,
744
745 #[clap(long = "uefi-enable-memory-protections", hide = true, requires("uefi"))]
747 pub deprecated_uefi_enable_memory_protections: bool,
748
749 #[clap(long = "uefi-force-dma-bounce", hide = true, requires("uefi"))]
751 pub deprecated_uefi_force_dma_bounce: bool,
752
753 #[clap(long = "uefi-force-firmware-version", hide = true, requires("uefi"))]
755 pub deprecated_uefi_force_firmware_version: bool,
756
757 #[clap(long, requires("pcat"))]
768 pub pcat_boot_order: Option<PcatBootOrderCli>,
769
770 #[clap(long, conflicts_with("uefi"))]
772 pub pcat: bool,
773
774 #[clap(long, requires("pcat"), value_name = "FILE")]
776 pub pcat_firmware: Option<PathBuf>,
777
778 #[clap(
780 long,
781 conflicts_with_all = ["kernel", "pcat"],
782 value_name = "OPTIONS",
783 long_help = r#"Boot an IGVM file.
784
785Syntax: --igvm firmware=<FILE>,personality=<openhcl|uefi|linux-direct>
786
787Both firmware and personality are required; neither has a default.
788The openhcl personality requires explicit --hv --vtl2."#
789 )]
790 pub igvm: Option<IgvmCli>,
791
792 #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
795 pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
796
797 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
802 pub virtio_9p: Vec<FsArgs>,
803
804 #[clap(long)]
806 pub virtio_9p_debug: bool,
807
808 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
813 pub virtio_fs: Vec<FsArgsWithOptions>,
814
815 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
820 pub virtio_fs_shmem: Vec<FsArgs>,
821
822 #[clap(long, value_name = "BUS", default_value = "auto")]
824 pub virtio_fs_bus: VirtioBusCli,
825
826 #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
831 pub virtio_pmem: Option<VirtioPmemArgs>,
832
833 #[clap(long)]
835 pub virtio_rng: bool,
836
837 #[clap(long, value_name = "BUS", default_value = "auto")]
839 pub virtio_rng_bus: VirtioBusCli,
840
841 #[clap(long, value_name = "PORT", requires("virtio_rng"))]
843 pub virtio_rng_pcie_port: Option<String>,
844
845 #[clap(
848 long,
849 value_name = "OPTIONS",
850 num_args = 0..=1,
851 default_missing_value = ""
852 )]
853 pub virtio_rtc: Option<VirtioRtcCli>,
854
855 #[clap(long)]
861 pub virtio_console: Option<SerialConfigCli>,
862
863 #[clap(long, value_name = "PORT", requires("virtio_console"))]
865 pub virtio_console_pcie_port: Option<String>,
866
867 #[clap(long, value_name = "BUS", value_parser = parse_virtio_vsock_bus)]
869 pub virtio_vsock_bus: Option<VirtioBusCli>,
870
871 #[clap(long, value_name = "PATH")]
873 pub virtio_vsock_path: Option<String>,
874
875 #[cfg(target_os = "linux")]
878 #[clap(
879 long,
880 value_name = "CID",
881 conflicts_with = "virtio_vsock_path",
882 value_parser = parse_vhost_vsock_cid
883 )]
884 pub virtio_vsock_vhost_cid: Option<u32>,
885
886 #[clap(long)]
894 pub virtio_net: Vec<NicConfigCli>,
895
896 #[clap(long, value_name = "PATH")]
898 pub log_file: Option<PathBuf>,
899
900 #[clap(long, value_name = "PATH")]
904 pub pidfile: Option<PathBuf>,
905
906 #[clap(long, value_name = "SOCKETPATH")]
910 pub ttrpc: Option<PathBuf>,
911
912 #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
916 pub grpc: Option<PathBuf>,
917
918 #[clap(long_help = r#"
920Run as an RPC server on the specified Unix socket.
921
922syntax: path=<PATH>[,transport=<TRANSPORT>]
923
924options:
925 `path=<PATH>` Unix socket path to listen on (required)
926 `transport=<TRANSPORT>` wire transport to accept (default: auto)
927
928valid transports:
929 `auto` auto-detect ttrpc vs. gRPC per connection
930 `ttrpc` accept ttrpc clients only
931 `grpc` accept gRPC clients only
932
933Examples:
934 --rpc path=/tmp/openvmm.sock
935 --rpc path=/tmp/openvmm.sock,transport=ttrpc
936"#)]
937 #[clap(
938 long,
939 value_name = "path=PATH[,transport=TRANSPORT]",
940 conflicts_with("ttrpc"),
941 conflicts_with("grpc")
942 )]
943 pub rpc: Option<RpcCli>,
944
945 #[clap(long)]
947 pub single_process: bool,
948
949 #[cfg(windows)]
951 #[clap(long, value_name = "PATH")]
952 pub device: Vec<String>,
953
954 #[clap(long = "disable-frontpage", hide = true, requires("uefi"))]
956 pub deprecated_disable_frontpage: bool,
957
958 #[clap(
960 long,
961 value_name = "VERSION",
962 num_args = 0..=1,
963 default_missing_value = "185"
964 )]
965 pub tpm: Option<TpmVersionCli>,
966
967 #[clap(long, default_value = "control", hide(true))]
971 #[expect(clippy::option_option)]
972 pub internal_worker: Option<Option<String>>,
973
974 #[clap(long, requires("vtl2"))]
976 pub vmbus_redirect: bool,
977
978 #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
980 pub vmbus_max_version: Option<u32>,
981
982 #[clap(long_help = r#"
986e.g: --vmgs memdiff:file:/path/to/file.vmgs
987
988syntax: <path> | kind:<arg>[,flag]
989
990valid disk kinds:
991 `mem:<len>` memory backed disk
992 <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
993 `memdiff:<disk>[;create=<len>]` memory backed diff disk
994 <disk>: lower disk, e.g.: `file:base.img`
995 `file:<path>` file-backed disk
996 <path>: path to file
997
998flags:
999 `fmt` reprovision the VMGS before boot
1000 `fmt-on-fail` reprovision the VMGS before boot if it is corrupted
1001"#)]
1002 #[clap(long)]
1003 pub vmgs: Option<VmgsCli>,
1004
1005 #[clap(long, requires("vmgs"))]
1007 pub test_gsp_by_id: bool,
1008
1009 #[clap(long, requires("pcat"), value_name = "FILE")]
1011 pub vga_firmware: Option<PathBuf>,
1012
1013 #[clap(long)]
1015 pub secure_boot: bool,
1016
1017 #[clap(long)]
1019 pub secure_boot_template: Option<SecureBootTemplateCli>,
1020
1021 #[clap(long, value_name = "PATH")]
1023 pub custom_uefi_json: Option<PathBuf>,
1024
1025 #[clap(long, hide(true))]
1030 pub relay_console_path: Option<PathBuf>,
1031
1032 #[clap(long, hide(true))]
1036 pub relay_console_title: Option<String>,
1037
1038 #[clap(long, value_name = "PORT")]
1040 pub gdb: Option<u16>,
1041
1042 #[clap(long)]
1048 pub mana: Vec<NicConfigCli>,
1049
1050 #[clap(long)]
1065 pub hypervisor: Option<String>,
1066
1067 #[clap(long)]
1073 pub nested_virt: bool,
1074
1075 #[clap(long_help = r#"
1085e.g: --ide memdiff:file:/path/to/disk.vhd
1086
1087syntax: <path> | kind:<arg>[,flag,opt=arg,...]
1088
1089valid disk kinds:
1090 `mem:<len>` memory backed disk
1091 <len>: length of ramdisk, e.g.: `1G`
1092 `memdiff:<disk>` memory backed diff disk
1093 <disk>: lower disk, e.g.: `file:base.img`
1094 `file:<path>[;create=<len>]` file-backed disk
1095 <path>: path to file
1096 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
1097 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
1098 `blob:<type>:<url>` HTTP blob (read-only)
1099 <type>: `flat` or `vhd1`
1100 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
1101 <cipher>: `xts-aes-256`
1102
1103additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
1104this list is not exhaustive.
1105
1106flags:
1107 `ro` open disk as read-only
1108 `s` attach drive to secondary ide channel
1109 `dvd` specifies that device is cd/dvd and it is read_only
1110"#)]
1111 #[clap(long, value_name = "FILE", requires("pcat"))]
1112 pub ide: Vec<IdeDiskCli>,
1113
1114 #[clap(long_help = r#"
1117e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
1118
1119syntax: <path> | kind:<arg>[,flag,opt=arg,...]
1120
1121valid disk kinds:
1122 `mem:<len>` memory backed disk
1123 <len>: length of ramdisk, e.g.: `1G`
1124 `memdiff:<disk>` memory backed diff disk
1125 <disk>: lower disk, e.g.: `file:base.img`
1126 `file:<path>[;create=<len>]` file-backed disk
1127 <path>: path to file
1128 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
1129 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
1130 `blob:<type>:<url>` HTTP blob (read-only)
1131 <type>: `flat` or `vhd1`
1132 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
1133 <cipher>: `xts-aes-256`
1134
1135flags:
1136 `ro` open disk as read-only
1137"#)]
1138 #[clap(long, value_name = "FILE", requires("pcat"))]
1139 pub floppy: Vec<FloppyDiskCli>,
1140
1141 #[clap(long)]
1143 pub guest_watchdog: bool,
1144
1145 #[clap(long)]
1148 pub openhcl_dump_path: Option<PathBuf>,
1149
1150 #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action)]
1154 pub guest_reset_action: GuestPowerAction,
1155
1156 #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1160 pub guest_shutdown_action: GuestPowerAction,
1161
1162 #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1166 pub guest_crash_action: GuestPowerAction,
1167
1168 #[clap(long, value_name = "PATH")]
1176 pub crash_dump_path: Option<PathBuf>,
1177
1178 #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action, requires = "guest_watchdog")]
1182 pub guest_watchdog_action: GuestPowerAction,
1183
1184 #[clap(long)]
1186 pub write_saved_state_proto: Option<PathBuf>,
1187
1188 #[clap(long)]
1190 pub imc: Option<PathBuf>,
1191
1192 #[clap(long)]
1194 pub battery: bool,
1195
1196 #[clap(long)]
1198 pub hibernation: bool,
1199
1200 #[clap(long = "uefi-console-mode", hide = true, requires("uefi"))]
1202 pub deprecated_uefi_console_mode: Option<UefiConsoleModeCli>,
1203
1204 #[clap(long_help = r#"
1206Set the EFI diagnostics log level.
1207
1208options:
1209 default default (ERROR and WARN only)
1210 info info (ERROR, WARN, and INFO)
1211 full full (all log levels)
1212"#)]
1213 #[clap(long = "efi-diagnostics-log-level", hide = true, requires("uefi"))]
1214 pub deprecated_efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
1215
1216 #[clap(long = "default-boot-always-attempt", hide = true)]
1218 pub deprecated_default_boot_always_attempt: bool,
1219
1220 #[cfg(guest_arch = "x86_64")]
1226 #[clap(long)]
1227 pub amd_iommu: Vec<String>,
1228
1229 #[cfg(guest_arch = "x86_64")]
1235 #[clap(long)]
1236 pub intel_vtd: Vec<String>,
1237
1238 #[clap(long_help = r#"
1240Attach root complexes to the VM.
1241
1242Examples:
1243 # Attach root complex rc0 on segment 0 with bus and MMIO ranges
1244 --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
1245
1246 # Configure HDM window size and restrictions (bitmask)
1247 --pcie-root-complex rc1,hdm=2G,hdm_window_restrictions=0x21
1248
1249Syntax: <name>[,opt=arg,...]
1250
1251Options:
1252 `segment=<value>` configures the PCI Express segment, default 0
1253 `start_bus=<value>` lowest valid bus number, default 0
1254 `end_bus=<value>` highest valid bus number, default 255
1255 `low_mmio=<size>` low MMIO window size, default 64M
1256 `high_mmio=<size>` high MMIO window size, default 1G
1257 `low_mmio_base=<addr>` pin low MMIO window base address (0x-prefixed hex)
1258 `high_mmio_base=<addr>` pin high MMIO window base address (0x-prefixed hex)
1259 `hdm=<size>` HDM decoder MMIO window size (CFMWS window), default 1G
1260 `hdm_window_restrictions=<m>` CFMWS window restriction bitmask (u16, decimal or 0x-prefixed hex),
1261 default DEVICE_COHERENT (bit 0, value 0x1)
1262 `preserve_bars` keep pinned BARs at their assigned addresses
1263 `node=<value>` NUMA node the root complex is associated with
1264"#)]
1265 #[clap(long, conflicts_with("pcat"))]
1266 pub pcie_root_complex: Vec<PcieRootComplexCli>,
1267
1268 #[clap(long, requires("pcie_root_complex"), conflicts_with("pcat"))]
1270 pub pcie_ecam_below_4gb: bool,
1271
1272 #[clap(long_help = r#"
1274Attach root ports to root complexes.
1275
1276Examples:
1277 # Attach root port rc0rp0 to root complex rc0
1278 --pcie-root-port rc0:rc0rp0
1279
1280 # Attach root port rc0rp1 to root complex rc0 with hotplug support
1281 --pcie-root-port rc0:rc0rp1,hotplug
1282
1283 # Attach root port rc0rp2 at device 5, function 0
1284 --pcie-root-port rc0:rc0rp2,addr=5
1285
1286 # Attach root port rc0rp3 at device 5, function 1
1287 --pcie-root-port rc0:rc0rp3,addr=5.1
1288
1289Syntax: <root_complex_name>:<name>[,opt,opt=arg,...]
1290
1291Options:
1292 `addr=<dev>[.<fn>]` device/function to place this port at (default:
1293 lowest available); dev 0-31, fn 0-7
1294 `hotplug` enable hotplug support for this root port
1295 `acs=<mask>` ACS capability bitmask (u16, decimal or 0x-prefixed hex)
1296 `cxl` configure this root port as CXL-capable
1297 `pasid` configure this port to support PASID for downstream devices
1298"#)]
1299 #[clap(long, conflicts_with("pcat"))]
1300 pub pcie_root_port: Vec<PcieRootPortCli>,
1301
1302 #[clap(long_help = r#"
1304Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
1305
1306Examples:
1307 # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
1308 --pcie-switch rp0:switch0,num_downstream_ports=4
1309
1310 # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
1311 --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
1312
1313 # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
1314 --pcie-switch rp0:switch0
1315 --pcie-switch switch0-downstream-0:switch1
1316 --pcie-switch switch1-downstream-1:switch2
1317
1318 # Enable hotplug on all downstream switch ports of switch0
1319 --pcie-switch rp0:switch0,hotplug
1320
1321 # Enable PASID on all downstream switch ports of switch0
1322 --pcie-switch rp0:switch0,pasid
1323
1324Syntax: <port_name>:<name>[,opt,opt=arg,...]
1325
1326 port_name can be:
1327 - Root port name (e.g., "rp0") to connect directly to a root port
1328 - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
1329
1330Options:
1331 `hotplug` enable hotplug support for all downstream switch ports
1332 `num_downstream_ports=<value>` number of downstream ports, default 4
1333 `acs=<mask>` ACS capability bitmask for downstream switch ports
1334 `pasid` configure this port to support PASID for downstream devices
1335"#)]
1336 #[clap(long, conflicts_with("pcat"))]
1337 pub pcie_switch: Vec<GenericPcieSwitchCli>,
1338
1339 #[clap(long_help = r#"
1341Declare that the device directly behind a PCIe port is a generic initiator
1342(GI) for a NUMA node, generating an SRAT Generic Initiator Affinity structure.
1343
1344The port may be a root port or a switch downstream port, so this works for
1345devices that sit behind a switch (e.g. a GPU placed under a switch shared
1346with a NIC for peer-to-peer DMA). The port is resolved by name against the
1347live topology after switch downstream ports are enumerated.
1348
1349Examples:
1350 # The device behind switch downstream port sw1-downstream-0 is a generic
1351 # initiator for NUMA node 1
1352 --pcie-generic-initiator port=sw1-downstream-0,node=1
1353
1354 # Also works for a root port name
1355 --pcie-generic-initiator port=rp0,node=2
1356
1357Syntax: port=<port_name>,node=<node>
1358"#)]
1359 #[clap(
1360 long = "pcie-generic-initiator",
1361 value_name = "port=<name>,node=<node>",
1362 conflicts_with("pcat")
1363 )]
1364 pub pcie_generic_initiator: Vec<PcieGenericInitiatorCli>,
1365
1366 #[clap(long_help = r#"
1368Attach PCIe devices to root ports or downstream switch ports
1369which are implemented in a simulator running in a remote process.
1370
1371Examples:
1372 # Attach to root port rc0rp0 with default socket
1373 --pcie-remote rc0rp0
1374
1375 # Attach with custom socket address
1376 --pcie-remote rc0rp0,socket=0.0.0.0:48914
1377
1378 # Specify HU and controller identifiers
1379 --pcie-remote rc0rp0,hu=1,controller=0
1380
1381 # Multiple devices on different ports
1382 --pcie-remote rc0rp0,socket=0.0.0.0:48914
1383 --pcie-remote rc0rp1,socket=0.0.0.0:48915
1384
1385Syntax: <port_name>[,opt=arg,...]
1386
1387Options:
1388 `socket=<address>` TCP socket (default: localhost:48914)
1389 `hu=<value>` Hardware unit identifier (default: 0)
1390 `controller=<value>` Controller identifier (default: 0)
1391"#)]
1392 #[clap(long, conflicts_with("pcat"))]
1393 pub pcie_remote: Vec<PcieRemoteCli>,
1394
1395 #[clap(long, hide = true, value_name = "PORT_NAME", conflicts_with("pcat"))]
1398 pub usb_xhci: Vec<UsbXhciCli>,
1399
1400 #[clap(
1402 long,
1403 hide = true,
1404 value_name = "SPEC",
1405 requires("usb_xhci"),
1406 long_help = r#"Attach an initial USB device to a controller declared with --usb-xhci.
1407
1408Syntax: KIND[,on=PCIE_PORT][,connector=N][,OPTION=VALUE,...]
1409
1410With one controller, on defaults to its PCIe port; otherwise on is required.
1411Connectors are one-based and optional. Explicit numbers are reserved first;
1412omitted connectors use the first free number in command-line order.
1413An empty controller needs no --usb-device options.
1414
1415Kinds: mock-loopback, mock-hid, mock-storage, mock-mutt, mock-supermutt,
1416 winusb, winusb-all.
1417
1418The first four mocks accept vid=ID,pid=ID,serial=STRING overrides.
1419IDs accept decimal or 0x-prefixed hexadecimal values.
1420mock-storage also accepts blocks=N in 512-byte logical blocks.
1421mock-supermutt accepts no device-specific options.
1422winusb requires instance_id=ID, a Windows PnP instance ID.
1423winusb-all selects eligible present devices at startup and assigns free
1424connectors. It accepts optional on=PCIE_PORT, but no connector.
1425
1426Examples:
1427 --usb-xhci rc0rp0 --usb-device mock-hid --usb-device mock-storage
1428 --usb-device mock-storage,on=rc0rp0,connector=2,blocks=65536
1429 --usb-device 'winusb,instance_id=USB\DEVICE\INSTANCE'
1430 --usb-device winusb-all"#
1431 )]
1432 pub usb_device: Vec<UsbDeviceCli>,
1433
1434 #[clap(long_help = r#"
1436Assign a host PCI device to the guest via Linux VFIO.
1437
1438The device must be bound to vfio-pci on the host before starting the VM.
1439
1440Examples:
1441 --vfio host=0000:01:00.0,port=rp0
1442 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1443
1444Keys:
1445 host=<pci_bdf> (required) PCI address on the host
1446 port=<name> (required) Root port or downstream switch port name
1447 iommu=<id> (optional) Reference to an --iommu object. When present,
1448 uses VFIO cdev + iommufd instead of the legacy group path.
1449"#)]
1450 #[cfg(target_os = "linux")]
1451 #[clap(long, conflicts_with("pcat"))]
1452 pub vfio: Vec<VfioDeviceCli>,
1453
1454 #[clap(long_help = r#"
1456Declare an iommufd context. Opens /dev/iommu so it can be referenced by
1457--vfio devices via the iommu=<id> key. The associated IOAS is allocated
1458the first time a --vfio device referring to this id is opened.
1459
1460Requires Linux kernel >= 6.6 with iommufd support.
1461
1462Examples:
1463 --iommu id=iommu0 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1464
1465Syntax: id=<name>
1466"#)]
1467 #[cfg(target_os = "linux")]
1468 #[clap(long, conflicts_with("pcat"))]
1469 pub iommu: Vec<IommuCli>,
1470}
1471
1472impl Options {
1473 pub fn effective_uefi(&self) -> anyhow::Result<Option<UefiCli>> {
1475 let mut uefi = match &self.uefi {
1476 Some(uefi) => uefi.clone(),
1477 None => return Ok(None),
1478 };
1479
1480 if let Some(firmware) = &self.deprecated_uefi_firmware {
1481 if uefi.firmware.is_some() {
1482 anyhow::bail!("--uefi firmware=... conflicts with --uefi-firmware");
1483 }
1484 uefi.firmware = Some(firmware.clone());
1485 }
1486 if let Some(console) = self.deprecated_uefi_console_mode {
1487 if uefi.console.is_some() {
1488 anyhow::bail!("--uefi console=... conflicts with --uefi-console-mode");
1489 }
1490 uefi.console = Some(console);
1491 }
1492 if let Some(diagnostics) = self.deprecated_efi_diagnostics_log_level {
1493 if uefi.diagnostics.is_some() {
1494 anyhow::bail!("--uefi diagnostics=... conflicts with --efi-diagnostics-log-level");
1495 }
1496 uefi.diagnostics = Some(diagnostics);
1497 }
1498
1499 uefi.debug |= self.deprecated_uefi_debug;
1500 uefi.enable_memory_protections |= self.deprecated_uefi_enable_memory_protections;
1501 uefi.force_dma_bounce |= self.deprecated_uefi_force_dma_bounce;
1502 uefi.force_firmware_version |= self.deprecated_uefi_force_firmware_version;
1503 uefi.disable_frontpage |= self.deprecated_disable_frontpage;
1504 uefi.default_boot_always_attempt |= self.deprecated_default_boot_always_attempt;
1505 Ok(Some(uefi))
1506 }
1507
1508 pub fn memory_size(&self) -> u64 {
1510 self.memory.size.map(|m| m.0).unwrap_or(DEFAULT_MEMORY_SIZE)
1511 }
1512
1513 pub fn prefetch_memory(&self) -> bool {
1515 self.memory.prefetch || self.deprecated_prefetch
1516 }
1517
1518 pub fn private_memory(&self) -> bool {
1520 self.memory.shared == Some(false) || self.deprecated_private_memory
1521 }
1522
1523 pub fn transparent_hugepages(&self) -> bool {
1525 self.memory
1526 .transparent_hugepages
1527 .unwrap_or(!self.memory.hugepages)
1528 || self.deprecated_thp
1529 }
1530
1531 pub fn memory_backing_file(&self) -> Option<&PathBuf> {
1533 self.memory
1534 .file
1535 .as_ref()
1536 .or(self.deprecated_memory_backing_file.as_ref())
1537 }
1538
1539 pub fn validate_memory_options(&self) -> anyhow::Result<()> {
1547 if self.memory.file.is_some() && self.deprecated_memory_backing_file.is_some() {
1548 anyhow::bail!("--memory file=... conflicts with --memory-backing-file");
1549 }
1550 if self.memory.file.is_some() && self.restore_snapshot.is_some() {
1551 anyhow::bail!("--memory file=... conflicts with --restore-snapshot");
1552 }
1553 if self.memory.shared == Some(true) && self.deprecated_private_memory {
1554 anyhow::bail!("--memory shared=on conflicts with --private-memory");
1555 }
1556 Ok(())
1557 }
1558
1559 pub fn validate_isolation_options(&self) -> anyhow::Result<()> {
1561 if self.snp_restricted_injection && !matches!(self.isolation, Some(IsolationCli::Snp)) {
1562 anyhow::bail!("--snp-restricted-injection requires --isolation snp");
1563 }
1564 if self.snp_restricted_injection
1565 && self
1566 .hypervisor
1567 .as_deref()
1568 .and_then(|value| value.split(':').next())
1569 != Some("mshv")
1570 {
1571 anyhow::bail!("--snp-restricted-injection requires --hypervisor mshv");
1572 }
1573 if self.snp_restricted_injection
1574 && (self.uefi.is_some()
1575 || self.pcat
1576 || self.igvm.is_some()
1577 || self.restore_snapshot.is_some())
1578 {
1579 anyhow::bail!("--snp-restricted-injection requires Linux direct boot");
1580 }
1581 if matches!(self.isolation, Some(IsolationCli::Snp)) {
1582 if self.uefi.is_some() {
1583 anyhow::bail!("SNP isolation currently only supports Linux direct boot");
1584 }
1585 if self.memory.hugepages
1586 || self
1587 .numa
1588 .as_ref()
1589 .is_some_and(|nodes| nodes.iter().any(|node| node.memory.hugepages))
1590 {
1591 anyhow::bail!("SNP isolation currently does not support hugetlb memory");
1592 }
1593 }
1594 Ok(())
1595 }
1596
1597 pub fn validate_igvm_options(&self) -> anyhow::Result<()> {
1599 let Some(igvm) = &self.igvm else {
1600 return Ok(());
1601 };
1602 match igvm.personality {
1603 IgvmPersonalityCli::Openhcl => {
1604 anyhow::ensure!(
1605 self.hv && self.vtl2,
1606 "--igvm personality=openhcl requires --hv --vtl2"
1607 );
1608 }
1609 IgvmPersonalityCli::Uefi | IgvmPersonalityCli::LinuxDirect => {
1610 anyhow::ensure!(
1611 !self.vtl2,
1612 "--vtl2 requires --igvm personality=openhcl for IGVM boots"
1613 );
1614 anyhow::ensure!(
1615 self.uefi.is_none(),
1616 "--uefi is only supported with --igvm personality=openhcl"
1617 );
1618 }
1619 }
1620 Ok(())
1621 }
1622}
1623
1624#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
1625pub struct IgvmCli {
1626 pub firmware: PathBuf,
1627 pub personality: IgvmPersonalityCli,
1628}
1629
1630#[derive(Clone, Debug, Default, vmm_cli::KeyValueArgs)]
1631pub struct UefiCli {
1632 pub firmware: Option<PathBuf>,
1633 #[kv(flag)]
1634 pub debug: bool,
1635 #[kv(flag)]
1636 pub enable_memory_protections: bool,
1637 #[kv(flag)]
1638 pub force_dma_bounce: bool,
1639 #[kv(flag)]
1640 pub force_firmware_version: bool,
1641 #[kv(flag)]
1642 pub disable_frontpage: bool,
1643 pub console: Option<UefiConsoleModeCli>,
1644 pub diagnostics: Option<EfiDiagnosticsLogLevelCli>,
1645 #[kv(flag)]
1646 pub default_boot_always_attempt: bool,
1647}
1648
1649#[derive(Clone, Debug, PartialEq, Eq, vmm_cli::KeyValueArgs)]
1651pub struct VirtioRtcCli {
1652 #[kv(default = VirtioBusCli::Auto)]
1654 pub bus: VirtioBusCli,
1655}
1656
1657#[derive(Clone, Debug, PartialEq)]
1658pub struct FsArgs {
1659 pub tag: String,
1660 pub path: String,
1661 pub pcie_port: Option<String>,
1662}
1663
1664impl FromStr for FsArgs {
1665 type Err = anyhow::Error;
1666
1667 fn from_str(s: &str) -> Result<Self, Self::Err> {
1668 let (pcie_port, s) = parse_pcie_port_prefix(s);
1669 let mut s = s.split(',');
1670 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
1671 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
1672 };
1673 Ok(Self {
1674 tag: tag.to_owned(),
1675 path: path.to_owned(),
1676 pcie_port,
1677 })
1678 }
1679}
1680
1681#[derive(Clone, Debug, PartialEq)]
1682pub struct FsArgsWithOptions {
1683 pub tag: String,
1685 pub path: String,
1687 pub options: String,
1689 pub pcie_port: Option<String>,
1691}
1692
1693impl FromStr for FsArgsWithOptions {
1694 type Err = anyhow::Error;
1695
1696 fn from_str(s: &str) -> Result<Self, Self::Err> {
1697 let (pcie_port, s) = parse_pcie_port_prefix(s);
1698 let mut s = s.split(',');
1699 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
1700 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
1701 };
1702 let options = s.collect::<Vec<_>>().join(";");
1703 Ok(Self {
1704 tag: tag.to_owned(),
1705 path: path.to_owned(),
1706 options,
1707 pcie_port,
1708 })
1709 }
1710}
1711
1712#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1716pub enum GuestPowerAction {
1717 Reset,
1719 Halt,
1722 Exit(u8),
1724}
1725
1726fn parse_guest_power_action(s: &str) -> Result<GuestPowerAction, String> {
1729 match s {
1730 "reset" => Ok(GuestPowerAction::Reset),
1731 "halt" => Ok(GuestPowerAction::Halt),
1732 "exit" => Ok(GuestPowerAction::Exit(0)),
1733 _ => match s.strip_prefix("exit:") {
1734 Some(code) => code
1735 .parse::<u8>()
1736 .map(GuestPowerAction::Exit)
1737 .map_err(|err| format!("invalid exit code '{code}' (expected 0-255): {err}")),
1738 None => Err(format!(
1739 "expected reset, halt, exit, or exit:<code>, got '{s}'"
1740 )),
1741 },
1742 }
1743}
1744
1745#[derive(Clone, Debug, PartialEq, Eq)]
1746pub enum VirtioBusCli {
1747 Auto,
1748 Mmio,
1749 Pci,
1750 Pcie(String),
1751 Vpci,
1752}
1753
1754impl FromStr for VirtioBusCli {
1755 type Err = String;
1756
1757 fn from_str(value: &str) -> Result<Self, Self::Err> {
1758 parse_virtio_bus(value, false)
1759 }
1760}
1761
1762fn parse_virtio_bus(value: &str, ignore_case: bool) -> Result<VirtioBusCli, String> {
1763 let eq = |value: &str, expected: &str| {
1764 if ignore_case {
1765 value.eq_ignore_ascii_case(expected)
1766 } else {
1767 value == expected
1768 }
1769 };
1770
1771 match value.split_once(':') {
1772 None if eq(value, "auto") => Ok(VirtioBusCli::Auto),
1773 None if eq(value, "mmio") => Ok(VirtioBusCli::Mmio),
1774 None if eq(value, "pci") => Ok(VirtioBusCli::Pci),
1775 None if eq(value, "vpci") => Ok(VirtioBusCli::Vpci),
1776 Some((bus, port)) if eq(bus, "pcie") && !port.is_empty() && !port.contains(':') => {
1777 Ok(VirtioBusCli::Pcie(port.to_string()))
1778 }
1779 _ => Err("expected auto, mmio, pci, pcie:<port>, or vpci".to_string()),
1780 }
1781}
1782
1783fn parse_virtio_vsock_bus(value: &str) -> Result<VirtioBusCli, String> {
1784 if value.eq_ignore_ascii_case("pcie") {
1785 return Ok(VirtioBusCli::Pcie("vsock".to_string()));
1786 }
1787
1788 match parse_virtio_bus(value, true) {
1789 Ok(bus @ (VirtioBusCli::Mmio | VirtioBusCli::Pci | VirtioBusCli::Pcie(_))) => Ok(bus),
1790 _ => Err("expected mmio, pci, pcie, or pcie:<port>".to_string()),
1791 }
1792}
1793
1794#[cfg(target_os = "linux")]
1795fn parse_vhost_vsock_cid(value: &str) -> Result<u32, String> {
1796 let cid = value
1797 .parse::<u32>()
1798 .map_err(|error| format!("invalid CID '{value}': {error}"))?;
1799 if !(3..u32::MAX).contains(&cid) {
1800 return Err(format!("CID must be between 3 and {}", u32::MAX - 1));
1801 }
1802 Ok(cid)
1803}
1804
1805fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
1810 if let Some(rest) = s.strip_prefix("pcie_port=") {
1811 if let Some((port, rest)) = rest.split_once(':') {
1812 if !port.is_empty() {
1813 return (Some(port.to_string()), rest);
1814 }
1815 }
1816 }
1817 (None, s)
1818}
1819
1820#[derive(Clone, Debug, PartialEq)]
1821pub struct VirtioPmemArgs {
1822 pub path: String,
1823 pub pcie_port: Option<String>,
1824}
1825
1826impl FromStr for VirtioPmemArgs {
1827 type Err = anyhow::Error;
1828
1829 fn from_str(s: &str) -> Result<Self, Self::Err> {
1830 let (pcie_port, s) = parse_pcie_port_prefix(s);
1831 if s.is_empty() {
1832 anyhow::bail!("expected [pcie_port=<port>:]<path>");
1833 }
1834 Ok(Self {
1835 path: s.to_owned(),
1836 pcie_port,
1837 })
1838 }
1839}
1840
1841#[derive(clap::ValueEnum, Clone, Copy)]
1842pub enum SecureBootTemplateCli {
1843 Windows,
1844 UefiCa,
1845}
1846
1847#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
1849pub enum TpmVersionCli {
1850 #[value(name = "138", alias = "1.38")]
1852 V138,
1853 #[value(name = "185", alias = "1.85")]
1855 V185,
1856}
1857
1858fn parse_memory(s: &str) -> anyhow::Result<u64> {
1859 if s == "VMGS_DEFAULT" {
1860 Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
1861 } else {
1862 || -> Option<u64> {
1863 let mut b = s.as_bytes();
1864 if s.ends_with('B') {
1865 b = &b[..b.len() - 1]
1866 }
1867 if b.is_empty() {
1868 return None;
1869 }
1870 let multi = match b[b.len() - 1] as char {
1871 'T' => Some(1024 * 1024 * 1024 * 1024),
1872 'G' => Some(1024 * 1024 * 1024),
1873 'M' => Some(1024 * 1024),
1874 'K' => Some(1024),
1875 _ => None,
1876 };
1877 if multi.is_some() {
1878 b = &b[..b.len() - 1]
1879 }
1880 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
1881 n.checked_mul(multi.unwrap_or(1))
1882 }()
1883 .with_context(|| format!("invalid memory size '{0}'", s))
1884 }
1885}
1886
1887fn parse_address(s: &str) -> anyhow::Result<u64> {
1889 let hex = s
1890 .strip_prefix("0x")
1891 .or_else(|| s.strip_prefix("0X"))
1892 .with_context(|| format!("invalid address '{s}', expected a 0x-prefixed hex value"))?;
1893 u64::from_str_radix(hex, 16).with_context(|| format!("invalid address '{s}'"))
1894}
1895
1896fn parse_acs_capability_mask(value: &str) -> anyhow::Result<u16> {
1897 if let Some(hex) = value
1898 .strip_prefix("0x")
1899 .or_else(|| value.strip_prefix("0X"))
1900 {
1901 u16::from_str_radix(hex, 16).context("invalid ACS capability mask")
1902 } else {
1903 value.parse::<u16>().context("invalid ACS capability mask")
1904 }
1905}
1906
1907#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1910pub struct SmbiosRelease(pub u8, pub u8);
1911
1912impl FromStr for SmbiosRelease {
1913 type Err = anyhow::Error;
1914
1915 fn from_str(value: &str) -> anyhow::Result<Self> {
1916 let (major, minor) = value
1917 .split_once('.')
1918 .with_context(|| format!("invalid smbios release '{value}', expected MAJOR.MINOR"))?;
1919 let major = major
1920 .parse::<u8>()
1921 .with_context(|| format!("invalid smbios release major '{major}'"))?;
1922 let minor = minor
1923 .parse::<u8>()
1924 .with_context(|| format!("invalid smbios release minor '{minor}'"))?;
1925 Ok(SmbiosRelease(major, minor))
1926 }
1927}
1928
1929#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1932pub enum SmbiosUuid {
1933 Random,
1935 Fixed(Guid),
1937}
1938
1939impl FromStr for SmbiosUuid {
1940 type Err = anyhow::Error;
1941
1942 fn from_str(value: &str) -> anyhow::Result<Self> {
1943 if value == "random" {
1944 Ok(SmbiosUuid::Random)
1945 } else {
1946 let guid = value
1947 .parse::<Guid>()
1948 .with_context(|| format!("invalid smbios uuid '{value}'"))?;
1949 Ok(SmbiosUuid::Fixed(guid))
1950 }
1951 }
1952}
1953
1954#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
1957pub struct SmbiosBiosCli {
1958 pub vendor: Option<String>,
1960 pub version: Option<String>,
1962 #[kv(key = "date")]
1964 pub release_date: Option<String>,
1965 pub release: Option<SmbiosRelease>,
1967}
1968
1969#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
1972pub struct SmbiosSystemCli {
1973 pub manufacturer: Option<String>,
1975 #[kv(key = "product")]
1977 pub product_name: Option<String>,
1978 pub version: Option<String>,
1980 #[kv(key = "serial")]
1982 pub serial_number: Option<String>,
1983 #[kv(key = "sku")]
1985 pub sku_number: Option<String>,
1986 pub family: Option<String>,
1988 pub uuid: Option<SmbiosUuid>,
1990}
1991
1992#[derive(Debug, Clone, Default, PartialEq, Eq)]
1999pub struct SmbiosCli {
2000 pub bios: SmbiosBiosCli,
2002 pub system: SmbiosSystemCli,
2004}
2005
2006fn smbios_set_once<T>(slot: &mut Option<T>, key: &str, value: T) -> anyhow::Result<()> {
2008 anyhow::ensure!(slot.is_none(), "duplicate smbios option '{key}'");
2009 *slot = Some(value);
2010 Ok(())
2011}
2012
2013impl SmbiosCli {
2014 pub fn merge(&mut self, other: SmbiosCli) -> anyhow::Result<()> {
2017 let SmbiosCli {
2018 bios:
2019 SmbiosBiosCli {
2020 vendor,
2021 version: bios_version,
2022 release_date,
2023 release,
2024 },
2025 system:
2026 SmbiosSystemCli {
2027 manufacturer,
2028 product_name,
2029 version: system_version,
2030 serial_number,
2031 sku_number,
2032 family,
2033 uuid,
2034 },
2035 } = other;
2036 merge_smbios_field(&mut self.bios.vendor, vendor, 0, "vendor")?;
2037 merge_smbios_field(&mut self.bios.version, bios_version, 0, "version")?;
2038 merge_smbios_field(&mut self.bios.release_date, release_date, 0, "date")?;
2039 merge_smbios_field(&mut self.bios.release, release, 0, "release")?;
2040 merge_smbios_field(
2041 &mut self.system.manufacturer,
2042 manufacturer,
2043 1,
2044 "manufacturer",
2045 )?;
2046 merge_smbios_field(&mut self.system.product_name, product_name, 1, "product")?;
2047 merge_smbios_field(&mut self.system.version, system_version, 1, "version")?;
2048 merge_smbios_field(&mut self.system.serial_number, serial_number, 1, "serial")?;
2049 merge_smbios_field(&mut self.system.sku_number, sku_number, 1, "sku")?;
2050 merge_smbios_field(&mut self.system.family, family, 1, "family")?;
2051 merge_smbios_field(&mut self.system.uuid, uuid, 1, "uuid")?;
2052 Ok(())
2053 }
2054}
2055
2056fn merge_smbios_field<T>(
2059 dst: &mut Option<T>,
2060 src: Option<T>,
2061 typ: u8,
2062 key: &str,
2063) -> anyhow::Result<()> {
2064 if let Some(value) = src {
2065 smbios_set_once(dst, &format!("type={typ} {key}"), value)?;
2066 }
2067 Ok(())
2068}
2069
2070fn parse_smbios(s: &str) -> anyhow::Result<SmbiosCli> {
2077 let mut typ: Option<u8> = None;
2078 let mut rest = Vec::new();
2079 for part in s.split(',') {
2080 let (key, value) = part
2081 .split_once('=')
2082 .with_context(|| format!("invalid smbios option '{part}', expected key=value"))?;
2083 if key.is_empty() || value.is_empty() {
2084 anyhow::bail!("invalid smbios option '{part}', expected key=value");
2085 }
2086 if key == "type" {
2087 anyhow::ensure!(typ.is_none(), "duplicate smbios option 'type'");
2088 typ = Some(
2089 value
2090 .parse::<u8>()
2091 .with_context(|| format!("invalid smbios type '{value}'"))?,
2092 );
2093 } else {
2094 rest.push(part);
2095 }
2096 }
2097
2098 let typ = typ.context("smbios option requires 'type=N' (e.g. 'type=1')")?;
2099 let rest = rest.join(",");
2100 Ok(match typ {
2101 0 => SmbiosCli {
2102 bios: rest.parse()?,
2103 ..Default::default()
2104 },
2105 1 => SmbiosCli {
2106 system: rest.parse()?,
2107 ..Default::default()
2108 },
2109 other => anyhow::bail!("unsupported smbios type '{other}' (expected 0 or 1)"),
2110 })
2111}
2112
2113fn parse_memory_config(s: &str) -> anyhow::Result<MemoryCli> {
2114 let memory = if !s.contains('=') && !s.contains(',') {
2116 MemoryCli {
2117 size: Some(s.parse::<vmm_cli::MemorySize>()?),
2118 ..Default::default()
2119 }
2120 } else {
2121 s.parse::<MemoryCli>()?
2122 };
2123 memory.validate()?;
2124 Ok(memory)
2125}
2126
2127fn parse_numa_node(s: &str) -> anyhow::Result<NumaNodeCli> {
2128 let node: NumaNodeCli = s.parse()?;
2129 anyhow::ensure!(
2130 node.memory.size.is_some(),
2131 "numa node requires 'size' option"
2132 );
2133 anyhow::ensure!(
2134 node.memory.file.is_none(),
2135 "'file' is not supported in --numa"
2136 );
2137 node.memory.validate()?;
2138 Ok(node)
2139}
2140
2141fn parse_numa_distance(s: &str) -> anyhow::Result<NumaDistanceCli> {
2142 let parts: Vec<&str> = s.split(':').collect();
2143 anyhow::ensure!(
2144 parts.len() == 3,
2145 "expected SRC:DST:DISTANCE format, got '{s}'"
2146 );
2147 let src = parts[0].parse::<u32>().context("invalid source node")?;
2148 let dst = parts[1]
2149 .parse::<u32>()
2150 .context("invalid destination node")?;
2151 let distance = parts[2].parse::<u8>().context("invalid distance")?;
2152 anyhow::ensure!(
2153 distance >= 10,
2154 "distance must be >= 10 (10 = local), got {distance}"
2155 );
2156 Ok(NumaDistanceCli { src, dst, distance })
2157}
2158
2159fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
2161 match s.strip_prefix("0x") {
2162 Some(rest) => u64::from_str_radix(rest, 16),
2163 None => s.parse::<u64>(),
2164 }
2165}
2166
2167#[derive(Clone, Debug, PartialEq)]
2168pub enum DiskCliKind {
2169 Memory(u64),
2171 MemoryDiff(Box<DiskCliKind>),
2173 Sqlite {
2175 path: PathBuf,
2176 create_with_len: Option<u64>,
2177 },
2178 SqliteDiff {
2180 path: PathBuf,
2181 create: bool,
2182 disk: Box<DiskCliKind>,
2183 },
2184 AutoCacheSqlite {
2186 cache_path: String,
2187 key: Option<String>,
2188 disk: Box<DiskCliKind>,
2189 },
2190 PersistentReservationsWrapper(Box<DiskCliKind>),
2192 File {
2194 path: PathBuf,
2195 create_with_len: Option<u64>,
2196 direct: bool,
2197 },
2198 Blob {
2200 kind: BlobKind,
2201 url: String,
2202 },
2203 Crypt {
2205 cipher: DiskCipher,
2206 key_file: PathBuf,
2207 disk: Box<DiskCliKind>,
2208 },
2209 DelayDiskWrapper {
2211 delay_ms: u64,
2212 disk: Box<DiskCliKind>,
2213 },
2214}
2215
2216#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
2217pub enum DiskCipher {
2218 #[clap(name = "xts-aes-256")]
2219 XtsAes256,
2220}
2221
2222#[derive(Copy, Clone, Debug, PartialEq)]
2223pub enum BlobKind {
2224 Flat,
2225 Vhd1,
2226}
2227
2228struct FileOpts {
2229 path: PathBuf,
2230 create_with_len: Option<u64>,
2231 direct: bool,
2232}
2233
2234fn parse_file_opts(arg: &str) -> anyhow::Result<FileOpts> {
2235 let mut path = arg;
2236 let mut create_with_len = None;
2237 let mut direct = false;
2238
2239 if let Some((p, rest)) = arg.split_once(';') {
2241 path = p;
2242 for opt in rest.split(';') {
2243 if let Some(len) = opt.strip_prefix("create=") {
2244 create_with_len = Some(parse_memory(len)?);
2245 } else if opt == "direct" {
2246 direct = true;
2247 } else {
2248 anyhow::bail!("invalid file option '{opt}', expected 'create=<len>' or 'direct'");
2249 }
2250 }
2251 }
2252
2253 Ok(FileOpts {
2254 path: path.into(),
2255 create_with_len,
2256 direct,
2257 })
2258}
2259
2260impl DiskCliKind {
2261 fn parse_autocache(
2264 arg: &str,
2265 cache_path: Result<String, std::env::VarError>,
2266 ) -> anyhow::Result<Self> {
2267 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
2268 let cache_path = cache_path.context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
2269 Ok(DiskCliKind::AutoCacheSqlite {
2270 cache_path,
2271 key: (!key.is_empty()).then(|| key.to_string()),
2272 disk: Box::new(kind.parse()?),
2273 })
2274 }
2275}
2276
2277impl FromStr for DiskCliKind {
2278 type Err = anyhow::Error;
2279
2280 fn from_str(s: &str) -> anyhow::Result<Self> {
2281 let disk = match s.split_once(':') {
2282 None => {
2284 let FileOpts {
2285 path,
2286 create_with_len,
2287 direct,
2288 } = parse_file_opts(s)?;
2289 DiskCliKind::File {
2290 path,
2291 create_with_len,
2292 direct,
2293 }
2294 }
2295 Some((kind, arg)) => match kind {
2296 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
2297 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
2298 "sql" => {
2299 let FileOpts {
2300 path,
2301 create_with_len,
2302 direct,
2303 } = parse_file_opts(arg)?;
2304 if direct {
2305 anyhow::bail!("'direct' is not supported for 'sql' disks");
2306 }
2307 DiskCliKind::Sqlite {
2308 path,
2309 create_with_len,
2310 }
2311 }
2312 "sqldiff" => {
2313 let (path_and_opts, kind) =
2314 arg.split_once(':').context("expected path[;opts]:kind")?;
2315 let disk = Box::new(kind.parse()?);
2316 match path_and_opts.split_once(';') {
2317 Some((path, create)) => {
2318 if create != "create" {
2319 anyhow::bail!("invalid syntax after ';', expected 'create'")
2320 }
2321 DiskCliKind::SqliteDiff {
2322 path: path.into(),
2323 create: true,
2324 disk,
2325 }
2326 }
2327 None => DiskCliKind::SqliteDiff {
2328 path: path_and_opts.into(),
2329 create: false,
2330 disk,
2331 },
2332 }
2333 }
2334 "autocache" => {
2335 Self::parse_autocache(arg, std::env::var("OPENVMM_AUTO_CACHE_PATH"))?
2336 }
2337 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
2338 "delay" => {
2339 let (delay_ms, kind) = arg.split_once(':').context("expected delay_ms:kind")?;
2340 DiskCliKind::DelayDiskWrapper {
2341 delay_ms: delay_ms.parse().context("invalid disk delay")?,
2342 disk: Box::new(kind.parse()?),
2343 }
2344 }
2345 "file" => {
2346 let FileOpts {
2347 path,
2348 create_with_len,
2349 direct,
2350 } = parse_file_opts(arg)?;
2351 DiskCliKind::File {
2352 path,
2353 create_with_len,
2354 direct,
2355 }
2356 }
2357 "blob" => {
2358 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
2359 let blob_kind = match blob_kind {
2360 "flat" => BlobKind::Flat,
2361 "vhd1" => BlobKind::Vhd1,
2362 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
2363 };
2364 DiskCliKind::Blob {
2365 kind: blob_kind,
2366 url: url.to_string(),
2367 }
2368 }
2369 "crypt" => {
2370 let (cipher, (key, kind)) = arg
2371 .split_once(':')
2372 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
2373 .context("expected cipher:key_file:kind")?;
2374 DiskCliKind::Crypt {
2375 cipher: ValueEnum::from_str(cipher, false)
2376 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
2377 key_file: PathBuf::from(key),
2378 disk: Box::new(kind.parse()?),
2379 }
2380 }
2381 kind => {
2382 let FileOpts {
2387 path,
2388 create_with_len,
2389 direct,
2390 } = parse_file_opts(s)?;
2391 if path.has_root() {
2392 DiskCliKind::File {
2393 path,
2394 create_with_len,
2395 direct,
2396 }
2397 } else {
2398 anyhow::bail!("invalid disk kind {kind}");
2399 }
2400 }
2401 },
2402 };
2403 Ok(disk)
2404 }
2405}
2406
2407#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2409pub enum RpcTransportCli {
2410 #[default]
2413 Auto,
2414 Ttrpc,
2416 Grpc,
2418}
2419
2420#[derive(Clone, Debug, PartialEq, Eq)]
2422pub struct RpcCli {
2423 pub path: PathBuf,
2425 pub transport: RpcTransportCli,
2427}
2428
2429impl FromStr for RpcCli {
2430 type Err = anyhow::Error;
2431
2432 fn from_str(s: &str) -> anyhow::Result<Self> {
2433 let mut path = None;
2434 let mut transport = None;
2435 for part in s.split(',') {
2436 let (key, value) = part
2437 .split_once('=')
2438 .with_context(|| format!("invalid rpc option '{part}', expected key=value"))?;
2439 match key {
2440 "path" => {
2441 anyhow::ensure!(path.is_none(), "duplicate option 'path'");
2442 anyhow::ensure!(!value.is_empty(), "'path' requires a value");
2443 path = Some(PathBuf::from(value));
2444 }
2445 "transport" => {
2446 anyhow::ensure!(transport.is_none(), "duplicate option 'transport'");
2447 transport = Some(match value {
2448 "auto" => RpcTransportCli::Auto,
2449 "ttrpc" => RpcTransportCli::Ttrpc,
2450 "grpc" => RpcTransportCli::Grpc,
2451 _ => anyhow::bail!(
2452 "invalid transport '{value}', expected auto, ttrpc, or grpc"
2453 ),
2454 });
2455 }
2456 _ => anyhow::bail!("unknown rpc option '{key}'"),
2457 }
2458 }
2459
2460 Ok(RpcCli {
2461 path: path.context("'path' is required")?,
2462 transport: transport.unwrap_or_default(),
2463 })
2464 }
2465}
2466
2467#[derive(Clone)]
2468pub struct VmgsCli {
2469 pub kind: DiskCliKind,
2470 pub provision: ProvisionCondition,
2471}
2472
2473#[derive(Copy, Clone)]
2474pub enum ProvisionCondition {
2475 OnEmpty,
2476 OnFailure,
2477 Single,
2478}
2479
2480impl FromStr for VmgsCli {
2481 type Err = anyhow::Error;
2482
2483 fn from_str(s: &str) -> anyhow::Result<Self> {
2484 let (kind, opt) = s
2485 .split_once(',')
2486 .map(|(k, o)| (k, Some(o)))
2487 .unwrap_or((s, None));
2488 let kind = kind.parse()?;
2489
2490 let provision = match opt {
2491 None => ProvisionCondition::OnEmpty,
2492 Some("fmt-on-fail") => ProvisionCondition::OnFailure,
2493 Some("fmt") => ProvisionCondition::Single,
2494 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
2495 };
2496
2497 Ok(VmgsCli { kind, provision })
2498 }
2499}
2500
2501#[derive(clap::Args)]
2503pub struct VncCli {
2504 #[clap(long)]
2506 pub vnc: bool,
2507
2508 #[clap(long, value_name = "PORT", default_value = "5900")]
2510 pub vnc_port: u16,
2511
2512 #[clap(long, value_name = "ADDRESS", default_value = "127.0.0.1")]
2516 pub vnc_listen: String,
2517
2518 #[clap(long, value_name = "COUNT", default_value = "16")]
2520 pub vnc_max_clients: usize,
2521
2522 #[clap(long)]
2525 pub vnc_evict_oldest: bool,
2526}
2527
2528#[derive(Clone)]
2530pub struct DiskCli {
2531 pub vtl: DeviceVtl,
2532 pub kind: DiskCliKind,
2533 pub read_only: bool,
2534 pub is_dvd: bool,
2535 pub underhill: Option<UnderhillDiskSource>,
2536 pub pcie_port: Option<String>,
2537 pub serial: Option<String>,
2538 pub controller: Option<String>,
2539 pub nsid: Option<u32>,
2540 pub lun: Option<u8>,
2541 pub relay: Option<(String, Option<u32>)>,
2542}
2543
2544#[derive(Copy, Clone)]
2545pub enum UnderhillDiskSource {
2546 Scsi,
2547 Nvme,
2548}
2549
2550struct RelayTarget {
2552 name: String,
2553 location: Option<u32>,
2554}
2555
2556impl FromStr for RelayTarget {
2557 type Err = anyhow::Error;
2558
2559 fn from_str(s: &str) -> anyhow::Result<Self> {
2560 Ok(if let Some((name, loc)) = s.split_once(':') {
2561 RelayTarget {
2562 name: name.to_string(),
2563 location: Some(loc.parse::<u32>().context("invalid relay location")?),
2564 }
2565 } else {
2566 RelayTarget {
2567 name: s.to_string(),
2568 location: None,
2569 }
2570 })
2571 }
2572}
2573
2574#[derive(vmm_cli::KeyValueArgs)]
2577struct DiskArgs {
2578 #[kv(positional)]
2579 kind: DiskCliKind,
2580 #[kv(flag)]
2581 ro: bool,
2582 #[kv(flag)]
2583 dvd: bool,
2584 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2585 vtl: DeviceVtl,
2586 #[kv(flag)]
2587 uh: bool,
2588 #[kv(flag, key = "uh-nvme")]
2589 uh_nvme: bool,
2590 pcie_port: Option<String>,
2591 serial: Option<String>,
2592 #[kv(key = "on")]
2593 controller: Option<String>,
2594 nsid: Option<u32>,
2595 lun: Option<u8>,
2596 relay: Option<RelayTarget>,
2597}
2598
2599impl FromStr for DiskCli {
2600 type Err = anyhow::Error;
2601
2602 fn from_str(s: &str) -> anyhow::Result<Self> {
2603 let args: DiskArgs = s.parse()?;
2604
2605 let underhill = match (args.uh, args.uh_nvme) {
2606 (false, false) => None,
2607 (true, false) => Some(UnderhillDiskSource::Scsi),
2608 (false, true) => Some(UnderhillDiskSource::Nvme),
2609 (true, true) => anyhow::bail!("`uh` and `uh-nvme` are mutually exclusive"),
2610 };
2611 let read_only = args.ro || args.dvd;
2612 let is_dvd = args.dvd;
2613 let vtl = args.vtl;
2614 let pcie_port = args.pcie_port;
2615 let serial = args.serial;
2616 let controller = args.controller;
2617 let nsid = args.nsid;
2618 let lun = args.lun;
2619 let relay = args.relay.map(|r| (r.name, r.location));
2620
2621 if serial.as_ref().is_some_and(|serial| {
2622 serial.bytes().any(|byte| {
2623 !(byte.is_ascii_graphic() || byte == b' ') || matches!(byte, b'[' | b']')
2624 })
2625 }) {
2626 anyhow::bail!("`serial` must contain only printable ASCII characters and no brackets");
2627 }
2628
2629 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
2630 anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
2631 }
2632
2633 if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
2634 anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
2635 }
2636
2637 if controller.is_some() && pcie_port.is_some() {
2638 anyhow::bail!("`on` is incompatible with `pcie_port`");
2639 }
2640
2641 if controller.is_some() && vtl != DeviceVtl::Vtl0 {
2642 anyhow::bail!(
2643 "`vtl2` is incompatible with `on`; the controller's VTL determines placement"
2644 );
2645 }
2646
2647 if controller.is_some() && underhill.is_some() {
2648 anyhow::bail!("`on` is incompatible with `uh` and `uh-nvme`; use `relay` instead");
2649 }
2650
2651 if nsid.is_some() && controller.is_none() {
2652 anyhow::bail!("`nsid` requires `on`");
2653 }
2654
2655 if lun.is_some() && controller.is_none() {
2656 anyhow::bail!("`lun` requires `on`");
2657 }
2658
2659 if nsid.is_some() && lun.is_some() {
2660 anyhow::bail!("`nsid` and `lun` are mutually exclusive");
2661 }
2662
2663 if relay.is_some() && controller.is_none() {
2664 anyhow::bail!("`relay` requires `on`");
2665 }
2666
2667 if relay.is_some() && underhill.is_some() {
2668 anyhow::bail!("`relay` is incompatible with `uh` and `uh-nvme`");
2669 }
2670
2671 Ok(DiskCli {
2672 vtl,
2673 kind: args.kind,
2674 read_only,
2675 is_dvd,
2676 underhill,
2677 pcie_port,
2678 serial,
2679 controller,
2680 nsid,
2681 lun,
2682 relay,
2683 })
2684 }
2685}
2686
2687#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueGroup)]
2689pub enum NvmeControllerTransport {
2690 #[kv(key = "pcie_port")]
2692 Pcie(String),
2693 #[kv(key = "vpci")]
2695 Vpci(Option<Guid>),
2696}
2697
2698#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2700pub struct NvmeControllerCli {
2701 pub id: String,
2703 #[kv(flatten)]
2705 pub transport: NvmeControllerTransport,
2706 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2708 pub vtl: DeviceVtl,
2709}
2710
2711#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2713pub struct ScsiControllerCli {
2714 pub id: String,
2716 #[kv(default)]
2718 pub sub_channels: u16,
2719 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2721 pub vtl: DeviceVtl,
2722}
2723
2724#[derive(Copy, Clone, Debug, PartialEq)]
2726pub enum OpenhclControllerType {
2727 Scsi,
2728 Nvme,
2729}
2730
2731#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2733pub struct OpenhclControllerCli {
2734 pub id: String,
2736 #[kv(key = "type")]
2738 pub controller_type: OpenhclControllerType,
2739 pub guid: Option<Guid>,
2741}
2742
2743impl FromStr for OpenhclControllerType {
2744 type Err = anyhow::Error;
2745
2746 fn from_str(s: &str) -> anyhow::Result<Self> {
2747 Ok(match s {
2748 "scsi" => OpenhclControllerType::Scsi,
2749 "nvme" => OpenhclControllerType::Nvme,
2750 other => anyhow::bail!("unknown controller type: '{other}'"),
2751 })
2752 }
2753}
2754
2755#[derive(Clone, Debug, PartialEq)]
2757pub struct CxlTestDeviceCli {
2758 pub hdm_size: u64,
2760 pub pcie_port: String,
2762}
2763
2764impl FromStr for CxlTestDeviceCli {
2765 type Err = anyhow::Error;
2766
2767 fn from_str(s: &str) -> anyhow::Result<Self> {
2768 let mut opts = s.split(',');
2769 let first = opts.next().context("expected CXL test device config")?;
2770 let (kind, arg) = first
2771 .split_once(':')
2772 .context("expected CXL test syntax: mem:<len>")?;
2773
2774 if kind != "mem" {
2775 anyhow::bail!("unsupported CXL test backing kind '{kind}', expected 'mem'");
2776 }
2777
2778 let hdm_size = parse_memory(arg).context("failed to parse CXL test HDM size")?;
2779 let mut pcie_port = None;
2780
2781 for opt in opts {
2782 let mut kv = opt.split('=');
2783 let key = kv.next().unwrap_or_default();
2784 match key {
2785 "pcie_port" => {
2786 let val = kv.next();
2787 if val.is_none_or(|v| v.is_empty()) {
2788 anyhow::bail!("`pcie_port` requires a port name");
2789 }
2790 pcie_port = Some(val.unwrap().to_string());
2791 }
2792 _ => anyhow::bail!("unknown option: '{opt}'"),
2793 }
2794 }
2795
2796 let Some(pcie_port) = pcie_port else {
2797 anyhow::bail!("`pcie_port=<name>` is required for `--cxl-test`");
2798 };
2799
2800 Ok(Self {
2801 hdm_size,
2802 pcie_port,
2803 })
2804 }
2805}
2806
2807#[derive(Clone)]
2809pub struct IdeDiskCli {
2810 pub kind: DiskCliKind,
2811 pub read_only: bool,
2812 pub channel: Option<u8>,
2813 pub device: Option<u8>,
2814 pub is_dvd: bool,
2815}
2816
2817impl FromStr for IdeDiskCli {
2818 type Err = anyhow::Error;
2819
2820 fn from_str(s: &str) -> anyhow::Result<Self> {
2821 let mut opts = s.split(',');
2822 let kind = opts.next().unwrap().parse()?;
2823
2824 let mut read_only = false;
2825 let mut channel = None;
2826 let mut device = None;
2827 let mut is_dvd = false;
2828 for opt in opts {
2829 let mut s = opt.split('=');
2830 let opt = s.next().unwrap();
2831 match opt {
2832 "ro" => read_only = true,
2833 "p" => channel = Some(0),
2834 "s" => channel = Some(1),
2835 "0" => device = Some(0),
2836 "1" => device = Some(1),
2837 "dvd" => {
2838 is_dvd = true;
2839 read_only = true;
2840 }
2841 _ => anyhow::bail!("unknown option: '{opt}'"),
2842 }
2843 }
2844
2845 Ok(IdeDiskCli {
2846 kind,
2847 read_only,
2848 channel,
2849 device,
2850 is_dvd,
2851 })
2852 }
2853}
2854
2855#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
2857pub struct FloppyDiskCli {
2858 #[kv(positional)]
2859 pub kind: DiskCliKind,
2860 #[kv(flag, key = "ro")]
2861 pub read_only: bool,
2862}
2863
2864#[derive(Clone)]
2865pub struct DebugconSerialConfigCli {
2866 pub port: u16,
2867 pub serial: SerialConfigCli,
2868}
2869
2870impl FromStr for DebugconSerialConfigCli {
2871 type Err = String;
2872
2873 fn from_str(s: &str) -> Result<Self, Self::Err> {
2874 let Some((port, serial)) = s.split_once(',') else {
2875 return Err("invalid format (missing comma between port and serial)".into());
2876 };
2877
2878 let port: u16 = parse_number(port)
2879 .map_err(|_| "could not parse port".to_owned())?
2880 .try_into()
2881 .map_err(|_| "port must be 16-bit")?;
2882 let serial: SerialConfigCli = serial.parse()?;
2883
2884 Ok(Self { port, serial })
2885 }
2886}
2887
2888#[derive(Clone, Debug, PartialEq)]
2891pub struct ComSerialConfigCli {
2892 pub debugger_mode: bool,
2894 pub backend: SerialConfigCli,
2896}
2897
2898impl FromStr for ComSerialConfigCli {
2899 type Err = String;
2900
2901 fn from_str(s: &str) -> Result<Self, Self::Err> {
2902 match s.strip_prefix("debugger-mode:") {
2903 Some(rest) => Ok(Self {
2904 debugger_mode: true,
2905 backend: rest.parse()?,
2906 }),
2907 None => Ok(Self {
2908 debugger_mode: false,
2909 backend: s.parse()?,
2910 }),
2911 }
2912 }
2913}
2914
2915#[derive(Clone, Debug, PartialEq)]
2917pub enum SerialConfigCli {
2918 None,
2919 Null,
2920 Console,
2921 NewConsole(Option<PathBuf>, Option<String>),
2922 Stderr,
2923 Pipe(PathBuf),
2924 Tcp(SocketAddr),
2925 File(PathBuf),
2926}
2927
2928impl FromStr for SerialConfigCli {
2929 type Err = String;
2930
2931 fn from_str(s: &str) -> Result<Self, Self::Err> {
2932 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
2933
2934 let first_key = match keyvalues.first() {
2935 Some(first_pair) => first_pair.0.as_str(),
2936 None => Err("invalid serial configuration: no values supplied")?,
2937 };
2938 let first_value = keyvalues.first().unwrap().1.as_ref();
2939
2940 let ret = match first_key {
2941 "none" => SerialConfigCli::None,
2942 "null" => SerialConfigCli::Null,
2943 "console" => SerialConfigCli::Console,
2944 "stderr" => SerialConfigCli::Stderr,
2945 "file" => match first_value {
2946 Some(path) => SerialConfigCli::File(path.into()),
2947 None => Err("invalid serial configuration: file requires a value")?,
2948 },
2949 "term" => {
2950 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
2952 let window_name = match window_name {
2953 Some((_, Some(name))) => Some(name.clone()),
2954 _ => None,
2955 };
2956
2957 SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
2958 }
2959 "listen" => match first_value {
2960 Some(path) if let Some(tcp) = path.strip_prefix("tcp:") => {
2961 let addr = tcp
2962 .parse()
2963 .map_err(|err| format!("invalid tcp address: {err}"))?;
2964 SerialConfigCli::Tcp(addr)
2965 }
2966 Some(path) => SerialConfigCli::Pipe(path.into()),
2967 None => Err(
2968 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
2969 )?,
2970 },
2971 _ => {
2972 return Err(format!(
2973 "invalid serial configuration: '{}' is not a known option",
2974 first_key
2975 ));
2976 }
2977 };
2978
2979 Ok(ret)
2980 }
2981}
2982
2983impl SerialConfigCli {
2984 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
2987 let mut ret = Vec::new();
2988
2989 for item in s.split(',') {
2991 let mut eqsplit = item.split('=');
2994 let key = eqsplit.next();
2995 let value = eqsplit.next();
2996
2997 if let Some(key) = key {
2998 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
2999 } else {
3000 return Err("invalid key=value pair in serial config".into());
3002 }
3003 }
3004 Ok(ret)
3005 }
3006}
3007
3008#[derive(Clone, Debug, PartialEq)]
3009pub enum EndpointConfigCli {
3010 None,
3011 Consomme {
3012 cidr: Option<String>,
3013 host_fwd: Vec<HostPortConfigCli>,
3014 },
3015 Dio {
3016 id: Option<String>,
3017 },
3018 Tap {
3019 name: String,
3020 },
3021}
3022
3023#[derive(Clone, Debug, PartialEq)]
3025pub struct HostPortConfigCli {
3026 pub protocol: HostPortProtocolCli,
3027 pub host_address: Option<std::net::IpAddr>,
3028 pub host_port: u16,
3029 pub guest_port: u16,
3030}
3031
3032#[derive(Clone, Debug, PartialEq)]
3034pub enum HostPortProtocolCli {
3035 Tcp,
3036 Udp,
3037}
3038
3039fn parse_hostfwd(s: &str) -> Result<HostPortConfigCli, String> {
3040 let (host_part, guest_part) = s.split_once('-').ok_or_else(|| {
3043 format!(
3044 "invalid hostfwd format '{s}', \
3045 expected 'proto:[hostaddr]:hostport-[guestaddr]:guestport'"
3046 )
3047 })?;
3048
3049 let (proto, host_addr_port) = host_part.split_once(':').ok_or_else(|| {
3051 format!("invalid hostfwd host part '{host_part}', expected 'proto:[hostaddr]:hostport'")
3052 })?;
3053 let protocol = match proto {
3054 "tcp" => HostPortProtocolCli::Tcp,
3055 "udp" => HostPortProtocolCli::Udp,
3056 other => {
3057 return Err(format!(
3058 "unknown hostfwd protocol '{other}', expected 'tcp' or 'udp'"
3059 ));
3060 }
3061 };
3062
3063 let (host_address, host_port) = parse_addr_port(host_addr_port)
3064 .map_err(|e| format!("invalid hostfwd host address/port: {e}"))?;
3065 let (_, guest_port) = parse_addr_port(guest_part)
3066 .map_err(|e| format!("invalid hostfwd guest address/port: {e}"))?;
3067
3068 Ok(HostPortConfigCli {
3069 protocol,
3070 host_address,
3071 host_port,
3072 guest_port,
3073 })
3074}
3075
3076fn parse_addr_port(s: &str) -> Result<(Option<std::net::IpAddr>, u16), String> {
3082 if let Some(rest) = s.strip_prefix('[') {
3083 let (addr, port) = rest
3085 .split_once("]:")
3086 .ok_or_else(|| format!("expected '[addr]:port', got '[{rest}'"))?;
3087 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
3088 let addr: std::net::IpAddr = addr
3089 .parse()
3090 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
3091 Ok((Some(addr), port))
3092 } else {
3093 match s.rsplit_once(':') {
3094 Some((addr, port)) => {
3095 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
3096 let addr = if addr.is_empty() {
3097 None
3098 } else {
3099 let parsed: std::net::IpAddr = addr
3100 .parse()
3101 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
3102 Some(parsed)
3103 };
3104 Ok((addr, port))
3105 }
3106 None => {
3107 let port: u16 = s.parse().map_err(|_| format!("invalid port '{s}'"))?;
3108 Ok((None, port))
3109 }
3110 }
3111 }
3112}
3113
3114impl FromStr for EndpointConfigCli {
3115 type Err = String;
3116
3117 fn from_str(s: &str) -> Result<Self, Self::Err> {
3118 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
3119 ["none"] => EndpointConfigCli::None,
3120 ["consomme", rest @ ..] => {
3121 let remaining = rest.join(":");
3122 let mut cidr = None;
3123 let mut host_fwd = Vec::new();
3124 for opt in remaining.split(',').filter(|s| !s.is_empty()) {
3125 if let Some(fwd) = opt.strip_prefix("hostfwd=") {
3126 host_fwd.push(parse_hostfwd(fwd)?);
3127 } else if cidr.is_none() {
3128 cidr = Some(opt.to_owned());
3129 } else {
3130 return Err(format!("unexpected consomme option '{opt}'"));
3131 }
3132 }
3133 EndpointConfigCli::Consomme { cidr, host_fwd }
3134 }
3135 ["dio", s @ ..] => EndpointConfigCli::Dio {
3136 id: s.first().map(|s| (*s).to_owned()),
3137 },
3138 ["tap", name] => EndpointConfigCli::Tap {
3139 name: (*name).to_owned(),
3140 },
3141 _ => return Err("invalid network backend".into()),
3142 };
3143
3144 Ok(ret)
3145 }
3146}
3147
3148#[derive(Clone, Debug, PartialEq)]
3149pub struct NicConfigCli {
3150 pub vtl: DeviceVtl,
3151 pub endpoint: EndpointConfigCli,
3152 pub mac_address: Option<MacAddress>,
3154 pub max_queues: Option<u16>,
3155 pub enable_rx_gso: bool,
3156 pub underhill: bool,
3157 pub pcie_port: Option<String>,
3158}
3159
3160impl FromStr for NicConfigCli {
3161 type Err = String;
3162
3163 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
3164 let mut vtl = DeviceVtl::Vtl0;
3165 let mut mac_address = None;
3166 let mut max_queues = None;
3167 let mut enable_rx_gso = false;
3168 let mut underhill = false;
3169 let mut pcie_port = None;
3170 while let Some((opt, rest)) = s.split_once(':') {
3171 if let Some((opt, val)) = opt.split_once('=') {
3172 match opt {
3173 "queues" => {
3174 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
3175 }
3176 "mac" => {
3177 mac_address = Some(val.parse().map_err(|_| "failed to parse MAC address")?);
3178 }
3179 "pcie_port" => {
3180 if val.is_empty() {
3181 return Err("`pcie_port=` requires port name argument".into());
3182 }
3183 pcie_port = Some(val.to_string());
3184 }
3185 _ => break,
3186 }
3187 } else {
3188 match opt {
3189 "vtl2" => {
3190 vtl = DeviceVtl::Vtl2;
3191 }
3192 "uh" => underhill = true,
3193 "rx_gso" => enable_rx_gso = true,
3194 _ => break,
3195 }
3196 }
3197 s = rest;
3198 }
3199
3200 if underhill && vtl != DeviceVtl::Vtl0 {
3201 return Err("`uh` is incompatible with `vtl2`".into());
3202 }
3203
3204 if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
3205 return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
3206 }
3207
3208 let endpoint = s.parse()?;
3209 Ok(NicConfigCli {
3210 vtl,
3211 endpoint,
3212 mac_address,
3213 max_queues,
3214 enable_rx_gso,
3215 underhill,
3216 pcie_port,
3217 })
3218 }
3219}
3220
3221#[derive(Debug, Error)]
3222#[error("unknown VTL2 relocation type: {0}")]
3223pub struct UnknownVtl2RelocationType(String);
3224
3225fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
3226 match s {
3227 "disable" => Ok(Vtl2BaseAddressType::File),
3228 s if s.starts_with("auto=") => {
3229 let s = s.strip_prefix("auto=").unwrap_or_default();
3230 let size = if s == "filesize" {
3231 None
3232 } else {
3233 let size = parse_memory(s).map_err(|e| {
3234 UnknownVtl2RelocationType(format!(
3235 "unable to parse memory size from {} for 'auto=' type, {e}",
3236 e
3237 ))
3238 })?;
3239 Some(size)
3240 };
3241 Ok(Vtl2BaseAddressType::MemoryLayout { size })
3242 }
3243 s if s.starts_with("absolute=") => {
3244 let s = s.strip_prefix("absolute=");
3245 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
3246 UnknownVtl2RelocationType(format!(
3247 "unable to parse number from {} for 'absolute=' type",
3248 e
3249 ))
3250 })?;
3251 Ok(Vtl2BaseAddressType::Absolute(addr))
3252 }
3253 s if s.starts_with("vtl2=") => {
3254 let s = s.strip_prefix("vtl2=").unwrap_or_default();
3255 let size = if s == "filesize" {
3256 None
3257 } else {
3258 let size = parse_memory(s).map_err(|e| {
3259 UnknownVtl2RelocationType(format!(
3260 "unable to parse memory size from {} for 'vtl2=' type, {e}",
3261 e
3262 ))
3263 })?;
3264 Some(size)
3265 };
3266 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
3267 }
3268 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
3269 }
3270}
3271
3272#[derive(Debug, Copy, Clone, PartialEq)]
3273pub enum SmtConfigCli {
3274 Auto,
3275 Force,
3276 Off,
3277}
3278
3279#[derive(Debug, Error)]
3280#[error("expected auto, force, or off")]
3281pub struct BadSmtConfig;
3282
3283impl FromStr for SmtConfigCli {
3284 type Err = BadSmtConfig;
3285
3286 fn from_str(s: &str) -> Result<Self, Self::Err> {
3287 let r = match s {
3288 "auto" => Self::Auto,
3289 "force" => Self::Force,
3290 "off" => Self::Off,
3291 _ => return Err(BadSmtConfig),
3292 };
3293 Ok(r)
3294 }
3295}
3296
3297#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
3298fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
3299 let r = match s {
3300 "auto" => X2ApicConfig::Auto,
3301 "supported" => X2ApicConfig::Supported,
3302 "off" => X2ApicConfig::Unsupported,
3303 "on" => X2ApicConfig::Enabled,
3304 _ => return Err("expected auto, supported, off, or on"),
3305 };
3306 Ok(r)
3307}
3308
3309#[derive(Debug, Copy, Clone, ValueEnum)]
3310pub enum Vtl0LateMapPolicyCli {
3311 Off,
3312 Log,
3313 Halt,
3314 Exception,
3315}
3316
3317#[derive(Debug, Copy, Clone, Default, ValueEnum)]
3319pub enum GicMsiCli {
3320 #[default]
3322 Auto,
3323 Its,
3325 V2m,
3327}
3328
3329#[derive(Debug, Copy, Clone, ValueEnum)]
3330pub enum IsolationCli {
3331 Vbs,
3332 Snp,
3333}
3334
3335#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
3336pub enum IgvmPersonalityCli {
3337 Openhcl,
3339 Uefi,
3341 LinuxDirect,
3343}
3344
3345impl FromStr for IgvmPersonalityCli {
3346 type Err = String;
3347
3348 fn from_str(value: &str) -> Result<Self, Self::Err> {
3349 ValueEnum::from_str(value, false)
3350 }
3351}
3352
3353#[derive(Debug, Copy, Clone, PartialEq)]
3354pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
3355
3356impl FromStr for PcatBootOrderCli {
3357 type Err = &'static str;
3358
3359 fn from_str(s: &str) -> Result<Self, Self::Err> {
3360 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
3361 let mut order = Vec::new();
3362
3363 for item in s.split(',') {
3364 let device = match item {
3365 "optical" => PcatBootDevice::Optical,
3366 "hdd" => PcatBootDevice::HardDrive,
3367 "net" => PcatBootDevice::Network,
3368 "floppy" => PcatBootDevice::Floppy,
3369 _ => return Err("unknown boot device type"),
3370 };
3371
3372 let default_pos = default_order
3373 .iter()
3374 .position(|x| x == &Some(device))
3375 .ok_or("cannot pass duplicate boot devices")?;
3376
3377 order.push(default_order[default_pos].take().unwrap());
3378 }
3379
3380 order.extend(default_order.into_iter().flatten());
3381 assert_eq!(order.len(), 4);
3382
3383 Ok(Self(order.try_into().unwrap()))
3384 }
3385}
3386
3387#[derive(Copy, Clone, Debug, ValueEnum)]
3388pub enum UefiConsoleModeCli {
3389 Default,
3390 Com1,
3391 Com2,
3392 None,
3393}
3394
3395impl FromStr for UefiConsoleModeCli {
3396 type Err = String;
3397
3398 fn from_str(value: &str) -> Result<Self, Self::Err> {
3399 ValueEnum::from_str(value, false)
3400 }
3401}
3402
3403#[derive(Copy, Clone, Debug, Default, ValueEnum)]
3404pub enum EfiDiagnosticsLogLevelCli {
3405 #[default]
3406 Default,
3407 Info,
3408 Full,
3409}
3410
3411impl FromStr for EfiDiagnosticsLogLevelCli {
3412 type Err = String;
3413
3414 fn from_str(value: &str) -> Result<Self, Self::Err> {
3415 ValueEnum::from_str(value, false)
3416 }
3417}
3418
3419#[derive(Clone, Debug, PartialEq)]
3420pub struct PcieRootComplexCli {
3421 pub name: String,
3422 pub segment: u16,
3423 pub start_bus: u8,
3424 pub end_bus: u8,
3425 pub low_mmio: u32,
3426 pub high_mmio: u64,
3427 pub low_mmio_base: Option<u64>,
3428 pub high_mmio_base: Option<u64>,
3429 pub preserve_bars: bool,
3430 pub hdm: u64,
3431 pub hdm_window_restrictions: CfmwsWindowRestrictions,
3432 pub vnode: Option<u32>,
3433}
3434
3435struct HexAddress(u64);
3437
3438impl FromStr for HexAddress {
3439 type Err = anyhow::Error;
3440
3441 fn from_str(s: &str) -> anyhow::Result<Self> {
3442 Ok(HexAddress(parse_address(s)?))
3443 }
3444}
3445
3446struct CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions);
3448
3449impl FromStr for CfmwsWindowRestrictionsCli {
3450 type Err = anyhow::Error;
3451
3452 fn from_str(s: &str) -> anyhow::Result<Self> {
3453 Ok(CfmwsWindowRestrictionsCli(
3454 parse_cxl_cfmws_window_restriction_u16_bitmask(s)?,
3455 ))
3456 }
3457}
3458
3459#[derive(vmm_cli::KeyValueArgs)]
3462struct PcieRootComplexArgs {
3463 #[kv(positional)]
3464 name: String,
3465 #[kv(default)]
3466 segment: u16,
3467 #[kv(default)]
3468 start_bus: u8,
3469 #[kv(default = 255)]
3470 end_bus: u8,
3471 #[kv(default = vmm_cli::MemorySize(64 * 1024 * 1024))]
3472 low_mmio: vmm_cli::MemorySize,
3473 #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
3474 high_mmio: vmm_cli::MemorySize,
3475 low_mmio_base: Option<HexAddress>,
3476 high_mmio_base: Option<HexAddress>,
3477 #[kv(flag)]
3478 preserve_bars: bool,
3479 #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
3480 hdm: vmm_cli::MemorySize,
3481 #[kv(default = CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions::DEVICE_COHERENT))]
3482 hdm_window_restrictions: CfmwsWindowRestrictionsCli,
3483 #[kv(key = "node")]
3484 vnode: Option<u32>,
3485}
3486
3487impl FromStr for PcieRootComplexCli {
3488 type Err = anyhow::Error;
3489
3490 fn from_str(s: &str) -> Result<Self, Self::Err> {
3491 let args: PcieRootComplexArgs = s.parse()?;
3492
3493 if args.start_bus > args.end_bus {
3494 anyhow::bail!("start_bus must be <= end_bus");
3495 }
3496
3497 let low_mmio = u32::try_from(args.low_mmio.0).context("low MMIO size exceeds 32 bits")?;
3498
3499 Ok(PcieRootComplexCli {
3500 name: args.name,
3501 segment: args.segment,
3502 start_bus: args.start_bus,
3503 end_bus: args.end_bus,
3504 low_mmio,
3505 high_mmio: args.high_mmio.0,
3506 low_mmio_base: args.low_mmio_base.map(|a| a.0),
3507 high_mmio_base: args.high_mmio_base.map(|a| a.0),
3508 preserve_bars: args.preserve_bars,
3509 hdm: args.hdm.0,
3510 hdm_window_restrictions: args.hdm_window_restrictions.0,
3511 vnode: args.vnode,
3512 })
3513 }
3514}
3515
3516fn parse_cxl_cfmws_window_restriction_u16_bitmask(
3517 s: &str,
3518) -> anyhow::Result<CfmwsWindowRestrictions> {
3519 let bits = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
3520 u16::from_str_radix(hex, 16).context("invalid hex bitmask")?
3521 } else {
3522 u16::from_str(s).context("invalid decimal bitmask")?
3523 };
3524
3525 CfmwsWindowRestrictions::try_from_bits(bits)
3526 .context("bitmask includes reserved CFMWS window restriction bits")
3527}
3528
3529#[derive(Clone, Debug, PartialEq)]
3530pub struct PcieRootPortCli {
3531 pub root_complex_name: String,
3532 pub name: String,
3533 pub devfn: Option<u8>,
3534 pub hotplug: bool,
3535 pub acs_capabilities_supported: Option<u16>,
3536 pub cxl: bool,
3537 pub pasid: bool,
3538}
3539
3540struct PortNamePair {
3543 parent: String,
3544 child: String,
3545}
3546
3547impl FromStr for PortNamePair {
3548 type Err = anyhow::Error;
3549
3550 fn from_str(s: &str) -> anyhow::Result<Self> {
3551 let mut it = s.split(':');
3552 let parent = it
3553 .next()
3554 .filter(|x| !x.is_empty())
3555 .context("expected parent name")?;
3556 let child = it
3557 .next()
3558 .filter(|x| !x.is_empty())
3559 .context("expected child name")?;
3560 anyhow::ensure!(it.next().is_none(), "unexpected token in '{s}'");
3561 Ok(PortNamePair {
3562 parent: parent.to_string(),
3563 child: child.to_string(),
3564 })
3565 }
3566}
3567
3568struct PcieAddr(u8);
3570
3571impl FromStr for PcieAddr {
3572 type Err = anyhow::Error;
3573
3574 fn from_str(s: &str) -> anyhow::Result<Self> {
3575 Ok(PcieAddr(parse_pcie_addr(s)?))
3576 }
3577}
3578
3579struct AcsMask(u16);
3581
3582impl FromStr for AcsMask {
3583 type Err = anyhow::Error;
3584
3585 fn from_str(s: &str) -> anyhow::Result<Self> {
3586 Ok(AcsMask(parse_acs_capability_mask(s)?))
3587 }
3588}
3589
3590#[derive(vmm_cli::KeyValueArgs)]
3592struct RootPortArgs {
3593 #[kv(positional)]
3594 names: PortNamePair,
3595 addr: Option<PcieAddr>,
3596 #[kv(flag)]
3597 hotplug: bool,
3598 acs: Option<AcsMask>,
3599 #[kv(flag)]
3600 cxl: bool,
3601 #[kv(flag)]
3602 pasid: bool,
3603}
3604
3605impl FromStr for PcieRootPortCli {
3606 type Err = anyhow::Error;
3607
3608 fn from_str(s: &str) -> Result<Self, Self::Err> {
3609 let args: RootPortArgs = s.parse()?;
3610 Ok(PcieRootPortCli {
3611 root_complex_name: args.names.parent,
3612 name: args.names.child,
3613 devfn: args.addr.map(|a| a.0),
3614 hotplug: args.hotplug,
3615 acs_capabilities_supported: args.acs.map(|a| a.0),
3616 cxl: args.cxl,
3617 pasid: args.pasid,
3618 })
3619 }
3620}
3621
3622fn parse_pcie_addr(s: &str) -> anyhow::Result<u8> {
3626 let parse_int = |v: &str| -> anyhow::Result<u8> {
3627 if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) {
3628 u8::from_str_radix(hex, 16).context("invalid hex number")
3629 } else {
3630 v.parse().context("invalid number")
3631 }
3632 };
3633
3634 let mut parts = s.split('.');
3635 let device = parse_int(parts.next().context("expected device number")?)?;
3636 let function = match parts.next() {
3637 Some(f) => parse_int(f)?,
3638 None => 0,
3639 };
3640 if parts.next().is_some() {
3641 anyhow::bail!("unexpected token in addr '{s}'");
3642 }
3643 if device > 31 {
3644 anyhow::bail!("device number {device} out of range (0-31)");
3645 }
3646 if function > 7 {
3647 anyhow::bail!("function number {function} out of range (0-7)");
3648 }
3649 Ok((device << 3) | function)
3650}
3651
3652#[derive(Clone, Debug, PartialEq)]
3653pub struct GenericPcieSwitchCli {
3654 pub port_name: String,
3655 pub name: String,
3656 pub num_downstream_ports: u8,
3657 pub hotplug: bool,
3658 pub acs_capabilities_supported: Option<u16>,
3659 pub pasid: bool,
3660}
3661
3662#[derive(vmm_cli::KeyValueArgs)]
3664struct SwitchArgs {
3665 #[kv(positional)]
3666 names: PortNamePair,
3667 #[kv(default = 4)]
3668 num_downstream_ports: u8,
3669 #[kv(flag)]
3670 hotplug: bool,
3671 acs: Option<AcsMask>,
3672 #[kv(flag)]
3673 pasid: bool,
3674}
3675
3676impl FromStr for GenericPcieSwitchCli {
3677 type Err = anyhow::Error;
3678
3679 fn from_str(s: &str) -> Result<Self, Self::Err> {
3680 let args: SwitchArgs = s.parse()?;
3681 Ok(GenericPcieSwitchCli {
3682 port_name: args.names.parent,
3683 name: args.names.child,
3684 num_downstream_ports: args.num_downstream_ports,
3685 hotplug: args.hotplug,
3686 acs_capabilities_supported: args.acs.map(|a| a.0),
3687 pasid: args.pasid,
3688 })
3689 }
3690}
3691
3692#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3694pub struct PcieGenericInitiatorCli {
3695 #[kv(key = "port")]
3698 pub port_name: String,
3699 pub node: u32,
3701}
3702
3703#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3705pub struct UsbXhciCli {
3706 #[kv(positional)]
3708 pub port_name: String,
3709}
3710
3711#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3713pub struct UsbIdCli(pub u16);
3714
3715impl FromStr for UsbIdCli {
3716 type Err = anyhow::Error;
3717
3718 fn from_str(value: &str) -> Result<Self, Self::Err> {
3719 Ok(Self(
3720 if let Some(hex) = value
3721 .strip_prefix("0x")
3722 .or_else(|| value.strip_prefix("0X"))
3723 {
3724 u16::from_str_radix(hex, 16).context("invalid hexadecimal USB ID")?
3725 } else {
3726 value.parse().context("invalid decimal USB ID")?
3727 },
3728 ))
3729 }
3730}
3731
3732#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3734pub struct UsbConnectorCli(u8);
3735
3736impl UsbConnectorCli {
3737 pub fn number(self) -> u8 {
3739 self.0
3740 }
3741}
3742
3743impl FromStr for UsbConnectorCli {
3744 type Err = anyhow::Error;
3745
3746 fn from_str(value: &str) -> Result<Self, Self::Err> {
3747 let connector = value.parse()?;
3748 anyhow::ensure!(connector != 0, "USB connector numbers are one-based");
3749 Ok(Self(connector))
3750 }
3751}
3752
3753#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
3755pub struct UsbDeviceLocationCli {
3756 pub on: Option<String>,
3758 pub connector: Option<UsbConnectorCli>,
3760}
3761
3762#[derive(Clone, Debug)]
3764pub enum UsbDeviceCli {
3765 Device {
3767 location: UsbDeviceLocationCli,
3769 backend: UsbBackend,
3771 },
3772 WinUsbAll {
3774 on: Option<String>,
3776 },
3777}
3778
3779impl UsbDeviceCli {
3780 pub fn controller_pcie_port(&self) -> Option<&str> {
3782 match self {
3783 Self::Device { location, .. } => location.on.as_deref(),
3784 Self::WinUsbAll { on } => on.as_deref(),
3785 }
3786 }
3787}
3788
3789#[derive(vmm_cli::KeyValueArgs)]
3791struct UsbMockArgs {
3792 #[kv(flatten)]
3794 location: UsbDeviceLocationCli,
3795 vid: Option<UsbIdCli>,
3797 pid: Option<UsbIdCli>,
3799 serial: Option<String>,
3801}
3802
3803impl UsbMockArgs {
3804 fn into_parts(
3805 self,
3806 ) -> (
3807 UsbDeviceLocationCli,
3808 usb_config_resources::UsbIdentityConfig,
3809 ) {
3810 (
3811 self.location,
3812 usb_config_resources::UsbIdentityConfig {
3813 vendor_id: self.vid.map(|id| id.0),
3814 product_id: self.pid.map(|id| id.0),
3815 serial_number: self.serial,
3816 },
3817 )
3818 }
3819}
3820
3821#[derive(vmm_cli::KeyValueArgs)]
3823struct UsbStorageArgs {
3824 #[kv(flatten)]
3826 mock: UsbMockArgs,
3827 blocks: Option<u64>,
3829}
3830
3831#[derive(vmm_cli::KeyValueArgs)]
3833struct UsbWinUsbArgs {
3834 #[kv(flatten)]
3836 location: UsbDeviceLocationCli,
3837 instance_id: String,
3839}
3840
3841#[derive(vmm_cli::KeyValueArgs)]
3843struct UsbWinUsbAllArgs {
3844 on: Option<String>,
3846}
3847
3848impl FromStr for UsbDeviceCli {
3849 type Err = anyhow::Error;
3850
3851 fn from_str(value: &str) -> Result<Self, Self::Err> {
3852 use usb_config_resources::*;
3853 let (kind, options) = value.split_once(',').unwrap_or((value, ""));
3854 let (location, backend) = match kind {
3855 "mock-loopback" => {
3856 let args: UsbMockArgs = options.parse()?;
3857 let (location, identity) = args.into_parts();
3858 (
3859 location,
3860 UsbBackend::Loopback(MockLoopbackConfig { identity }),
3861 )
3862 }
3863 "mock-hid" => {
3864 let args: UsbMockArgs = options.parse()?;
3865 let (location, identity) = args.into_parts();
3866 (location, UsbBackend::Hid(MockHidConfig { identity }))
3867 }
3868 "mock-storage" => {
3869 let args: UsbStorageArgs = options.parse()?;
3870 anyhow::ensure!(
3871 args.blocks != Some(0),
3872 "mock-storage blocks must be greater than zero"
3873 );
3874 let (location, identity) = args.mock.into_parts();
3875 (
3876 location,
3877 UsbBackend::Storage(MockStorageConfig {
3878 identity,
3879 block_count: args.blocks,
3880 }),
3881 )
3882 }
3883 "mock-mutt" => {
3884 let args: UsbMockArgs = options.parse()?;
3885 let (location, identity) = args.into_parts();
3886 (location, UsbBackend::Mutt(MockMuttConfig { identity }))
3887 }
3888 "mock-supermutt" => (options.parse()?, UsbBackend::SuperMutt(MockSuperMuttConfig)),
3889 "winusb" => {
3890 let args: UsbWinUsbArgs = options.parse()?;
3891 anyhow::ensure!(
3892 !args.instance_id.trim().is_empty() && !args.instance_id.contains('\0'),
3893 "WinUSB instance IDs must be nonempty and contain no NUL characters"
3894 );
3895 (
3896 args.location,
3897 UsbBackend::WinUsb(WinUsbConfig {
3898 instance_id: args.instance_id,
3899 }),
3900 )
3901 }
3902 "winusb-all" => {
3903 let args: UsbWinUsbAllArgs = options.parse()?;
3904 return Ok(Self::WinUsbAll { on: args.on });
3905 }
3906 _ => anyhow::bail!("unknown USB device kind {kind:?}"),
3907 };
3908 Ok(Self::Device { location, backend })
3909 }
3910}
3911
3912#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3914pub struct PcieRemoteCli {
3915 #[kv(positional)]
3917 pub port_name: String,
3918 #[kv(key = "socket")]
3920 pub socket_addr: Option<String>,
3921 #[kv(default)]
3923 pub hu: u16,
3924 #[kv(default)]
3926 pub controller: u16,
3927}
3928
3929#[cfg(target_os = "linux")]
3933#[derive(Clone, Debug)]
3934pub struct VfioDeviceCli {
3935 pub port_name: String,
3937 pub pci_id: String,
3939 pub iommu: Option<String>,
3942 pub bar_addresses: [vfio_assigned_device_resources::BarAddressConfig; 6],
3944}
3945
3946#[cfg(target_os = "linux")]
3948struct BarAddressCli(vfio_assigned_device_resources::BarAddressConfig);
3949
3950#[cfg(target_os = "linux")]
3951impl FromStr for BarAddressCli {
3952 type Err = anyhow::Error;
3953
3954 fn from_str(s: &str) -> anyhow::Result<Self> {
3955 let config = if s == "host" {
3956 vfio_assigned_device_resources::BarAddressConfig::HostAssigned
3957 } else if let Some(value) = s.strip_prefix("0x") {
3958 let address = u64::from_str_radix(value, 16).context("invalid BAR address")?;
3959 anyhow::ensure!(address != 0, "BAR address must be nonzero");
3960 vfio_assigned_device_resources::BarAddressConfig::Fixed(address)
3961 } else {
3962 anyhow::bail!("expected 'host' or a hexadecimal address starting with '0x'");
3963 };
3964 Ok(Self(config))
3965 }
3966}
3967
3968#[cfg(target_os = "linux")]
3971#[derive(vmm_cli::KeyValueArgs)]
3972struct BarFlags {
3973 bar0: Option<BarAddressCli>,
3974 bar1: Option<BarAddressCli>,
3975 bar2: Option<BarAddressCli>,
3976 bar3: Option<BarAddressCli>,
3977 bar4: Option<BarAddressCli>,
3978 bar5: Option<BarAddressCli>,
3979}
3980
3981#[cfg(target_os = "linux")]
3983#[derive(vmm_cli::KeyValueArgs)]
3984struct VfioArgs {
3985 host: String,
3986 port: String,
3987 iommu: Option<String>,
3988 #[kv(flatten)]
3989 bars: BarFlags,
3990}
3991
3992#[cfg(target_os = "linux")]
3993impl FromStr for VfioDeviceCli {
3994 type Err = anyhow::Error;
3995
3996 fn from_str(s: &str) -> Result<Self, Self::Err> {
3997 let args: VfioArgs = s.parse()?;
3998
3999 if args.host.contains('/') || args.host.contains("..") {
4001 anyhow::bail!("PCI address must not contain path separators");
4002 }
4003
4004 let bars = args.bars;
4005 let bar_addresses = [
4006 bars.bar0.map(|bar| bar.0).unwrap_or_default(),
4007 bars.bar1.map(|bar| bar.0).unwrap_or_default(),
4008 bars.bar2.map(|bar| bar.0).unwrap_or_default(),
4009 bars.bar3.map(|bar| bar.0).unwrap_or_default(),
4010 bars.bar4.map(|bar| bar.0).unwrap_or_default(),
4011 bars.bar5.map(|bar| bar.0).unwrap_or_default(),
4012 ];
4013
4014 Ok(VfioDeviceCli {
4015 port_name: args.port,
4016 pci_id: args.host,
4017 iommu: args.iommu,
4018 bar_addresses,
4019 })
4020 }
4021}
4022
4023#[cfg(guest_arch = "aarch64")]
4028#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
4029pub struct SmmuCli {
4030 #[kv(key = "rc")]
4032 pub rc_name: String,
4033 #[kv(flag)]
4035 pub accel: bool,
4036 #[kv(default)]
4038 pub oas: SmmuOasCli,
4039 #[kv(default)]
4041 pub ssidsize: SmmuSsidSizeCli,
4042}
4043
4044#[cfg(guest_arch = "aarch64")]
4046#[derive(Clone, Copy, Debug, Default)]
4047pub enum SmmuOasCli {
4048 #[default]
4051 Auto,
4052 Fixed(u8),
4054}
4055
4056#[cfg(guest_arch = "aarch64")]
4057impl FromStr for SmmuOasCli {
4058 type Err = anyhow::Error;
4059
4060 fn from_str(s: &str) -> Result<Self, Self::Err> {
4061 Ok(if s == "auto" {
4062 SmmuOasCli::Auto
4063 } else {
4064 SmmuOasCli::Fixed(s.parse().context("oas must be 'auto' or a number")?)
4065 })
4066 }
4067}
4068
4069#[cfg(guest_arch = "aarch64")]
4071#[derive(Clone, Copy, Debug, Default)]
4072pub enum SmmuSsidSizeCli {
4073 #[default]
4076 Auto,
4077 Fixed(u8),
4079}
4080
4081#[cfg(guest_arch = "aarch64")]
4082impl FromStr for SmmuSsidSizeCli {
4083 type Err = anyhow::Error;
4084
4085 fn from_str(s: &str) -> Result<Self, Self::Err> {
4086 Ok(if s == "auto" {
4087 Self::Auto
4088 } else {
4089 Self::Fixed(s.parse().context("ssidsize must be 'auto' or a number")?)
4090 })
4091 }
4092}
4093
4094#[cfg(target_os = "linux")]
4098#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
4099pub struct IommuCli {
4100 pub id: String,
4102}
4103
4104fn default_value_from_arch_env(name: &str) -> OsString {
4112 let prefix = if cfg!(guest_arch = "x86_64") {
4113 "X86_64"
4114 } else if cfg!(guest_arch = "aarch64") {
4115 "AARCH64"
4116 } else {
4117 return Default::default();
4118 };
4119 let prefixed = format!("{}_{}", prefix, name);
4120 std::env::var_os(name)
4121 .or_else(|| std::env::var_os(prefixed))
4122 .unwrap_or_default()
4123}
4124
4125pub fn default_uefi_firmware() -> Option<PathBuf> {
4126 OptionalPathBuf::from(default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE").as_os_str()).0
4127}
4128
4129#[derive(Clone)]
4131pub struct OptionalPathBuf(pub Option<PathBuf>);
4132
4133impl From<&std::ffi::OsStr> for OptionalPathBuf {
4134 fn from(s: &std::ffi::OsStr) -> Self {
4135 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
4136 }
4137}
4138
4139#[cfg(target_os = "linux")]
4140#[derive(Clone)]
4141pub enum VhostUserDeviceTypeCli {
4142 Blk {
4145 num_queues: Option<u16>,
4146 queue_size: Option<u16>,
4147 },
4148 Fs {
4150 tag: String,
4151 num_queues: Option<u16>,
4152 queue_size: Option<u16>,
4153 },
4154 Other {
4156 device_id: u16,
4157 queue_sizes: Vec<u16>,
4158 },
4159}
4160
4161#[cfg(target_os = "linux")]
4162#[derive(Clone)]
4163pub struct VhostUserCli {
4164 pub socket_path: String,
4165 pub device_type: VhostUserDeviceTypeCli,
4166 pub pcie_port: Option<String>,
4167}
4168
4169#[cfg(target_os = "linux")]
4172#[derive(vmm_cli::KeyValueArgs)]
4173struct VhostUserArgs {
4174 #[kv(positional)]
4175 socket_path: String,
4176 #[kv(key = "type")]
4177 type_name: Option<String>,
4178 device_id: Option<u16>,
4179 tag: Option<String>,
4180 pcie_port: Option<String>,
4181 num_queues: Option<u16>,
4182 queue_size: Option<u16>,
4183 queue_sizes: Option<vmm_cli::BracketList<u16>>,
4184}
4185
4186#[cfg(target_os = "linux")]
4187impl FromStr for VhostUserCli {
4188 type Err = anyhow::Error;
4189
4190 fn from_str(s: &str) -> anyhow::Result<Self> {
4191 let mut args: VhostUserArgs = s.parse()?;
4192 let type_name = args.type_name.take();
4193
4194 if type_name.is_some() == args.device_id.is_some() {
4195 anyhow::bail!("must specify type=<name> or device_id=<N>");
4196 }
4197
4198 let device_type = match type_name.as_deref() {
4201 Some("fs") => VhostUserDeviceTypeCli::Fs {
4202 tag: args.tag.take().context("type=fs requires tag=<name>")?,
4203 num_queues: args.num_queues.take(),
4204 queue_size: args.queue_size.take(),
4205 },
4206 Some("blk") => VhostUserDeviceTypeCli::Blk {
4207 num_queues: args.num_queues.take(),
4208 queue_size: args.queue_size.take(),
4209 },
4210 Some(ty) => anyhow::bail!("unknown vhost-user device type: '{ty}'"),
4211 None => {
4212 let queue_sizes = args
4213 .queue_sizes
4214 .take()
4215 .context("device_id= requires queue_sizes=[N,N,...]")?
4216 .0;
4217 anyhow::ensure!(!queue_sizes.is_empty(), "queue_sizes must be non-empty");
4218 VhostUserDeviceTypeCli::Other {
4219 device_id: args.device_id.unwrap(),
4220 queue_sizes,
4221 }
4222 }
4223 };
4224
4225 if args.tag.is_some() {
4226 anyhow::bail!("tag= is only valid for type=fs");
4227 }
4228 if args.queue_sizes.is_some() {
4229 anyhow::bail!("queue_sizes= is only valid for device_id=");
4230 }
4231 if args.num_queues.is_some() || args.queue_size.is_some() {
4232 anyhow::bail!(
4233 "num_queues= and queue_size= are not valid for device_id=; use queue_sizes="
4234 );
4235 }
4236
4237 Ok(VhostUserCli {
4238 socket_path: args.socket_path,
4239 device_type,
4240 pcie_port: args.pcie_port,
4241 })
4242 }
4243}
4244
4245#[cfg(test)]
4246mod tests {
4247 use super::*;
4248
4249 use std::path::Path;
4250 use test_with_tracing::test;
4251
4252 #[test]
4253 fn vmgs_provision_conditions() {
4254 for (option, expected) in [
4255 ("", ProvisionCondition::OnEmpty),
4256 (",fmt-on-fail", ProvisionCondition::OnFailure),
4257 (",fmt", ProvisionCondition::Single),
4258 ] {
4259 let vmgs: VmgsCli = format!("mem:VMGS_DEFAULT{option}").parse().unwrap();
4260 assert!(matches!(
4261 (vmgs.provision, expected),
4262 (ProvisionCondition::OnEmpty, ProvisionCondition::OnEmpty)
4263 | (ProvisionCondition::OnFailure, ProvisionCondition::OnFailure)
4264 | (ProvisionCondition::Single, ProvisionCondition::Single)
4265 ));
4266 }
4267 }
4268
4269 #[test]
4271 fn test_parse_usb_controller() {
4272 let controller = UsbXhciCli::from_str("rc0rp0").unwrap();
4273 assert_eq!(controller.port_name, "rc0rp0");
4274 assert!(UsbXhciCli::from_str("").is_err());
4275 assert!(UsbXhciCli::from_str("rc0rp0,connector=1").is_err());
4276 }
4277
4278 #[test]
4280 fn test_parse_usb_mock_defaults() {
4281 for kind in [
4282 "mock-loopback",
4283 "mock-hid",
4284 "mock-storage",
4285 "mock-mutt",
4286 "mock-supermutt",
4287 ] {
4288 let UsbDeviceCli::Device { location, backend } = UsbDeviceCli::from_str(kind).unwrap()
4289 else {
4290 panic!("expected a device for {kind}");
4291 };
4292 assert_eq!(location.on, None);
4293 assert_eq!(location.connector, None);
4294 let identity = match (kind, backend) {
4295 ("mock-loopback", UsbBackend::Loopback(config)) => config.identity,
4296 ("mock-hid", UsbBackend::Hid(config)) => config.identity,
4297 ("mock-storage", UsbBackend::Storage(config)) => {
4298 assert_eq!(config.block_count, None);
4299 config.identity
4300 }
4301 ("mock-mutt", UsbBackend::Mutt(config)) => config.identity,
4302 ("mock-supermutt", UsbBackend::SuperMutt(_)) => continue,
4303 _ => panic!("wrong backend variant for {kind}"),
4304 };
4305 assert_eq!(identity, usb_config_resources::UsbIdentityConfig::default());
4306 }
4307 }
4308
4309 #[test]
4311 fn test_parse_usb_mock_overrides() {
4312 for kind in ["mock-loopback", "mock-hid", "mock-storage", "mock-mutt"] {
4313 let capacity = if kind == "mock-storage" {
4314 ",blocks=4096"
4315 } else {
4316 ""
4317 };
4318 let selection = format!(
4319 "{kind},on=usb0,connector=7,vid=0x1234,pid=65535,serial=SERIAL42{capacity}"
4320 );
4321 let UsbDeviceCli::Device { location, backend } =
4322 UsbDeviceCli::from_str(&selection).unwrap()
4323 else {
4324 panic!("expected a device for {kind}");
4325 };
4326 assert_eq!(location.on.as_deref(), Some("usb0"));
4327 assert_eq!(location.connector.map(UsbConnectorCli::number), Some(7));
4328 let identity = match (kind, backend) {
4329 ("mock-loopback", UsbBackend::Loopback(config)) => config.identity,
4330 ("mock-hid", UsbBackend::Hid(config)) => config.identity,
4331 ("mock-storage", UsbBackend::Storage(config)) => {
4332 assert_eq!(config.block_count, Some(4096));
4333 config.identity
4334 }
4335 ("mock-mutt", UsbBackend::Mutt(config)) => config.identity,
4336 _ => panic!("wrong backend variant for {kind}"),
4337 };
4338 assert_eq!(identity.vendor_id, Some(0x1234));
4339 assert_eq!(identity.product_id, Some(65535));
4340 assert_eq!(identity.serial_number.as_deref(), Some("SERIAL42"));
4341 }
4342 }
4343
4344 #[test]
4346 fn test_parse_usb_host_selections() {
4347 for (selection, expected_pcie_port, expected_connector) in [
4348 (r"winusb,instance_id=USB\A&B C", None, None),
4349 (
4350 r"winusb,on=usb0,connector=3,instance_id=USB\A&B C",
4351 Some("usb0"),
4352 Some(3),
4353 ),
4354 ] {
4355 let UsbDeviceCli::Device {
4356 location,
4357 backend: UsbBackend::WinUsb(config),
4358 } = UsbDeviceCli::from_str(selection).unwrap()
4359 else {
4360 panic!("expected explicit WinUSB selection");
4361 };
4362 assert_eq!(location.on.as_deref(), expected_pcie_port);
4363 assert_eq!(
4364 location.connector.map(UsbConnectorCli::number),
4365 expected_connector
4366 );
4367 assert_eq!(config.instance_id, r"USB\A&B C");
4368 }
4369 for (selection, expected_pcie_port) in
4370 [("winusb-all", None), ("winusb-all,on=usb1", Some("usb1"))]
4371 {
4372 let UsbDeviceCli::WinUsbAll { on } = UsbDeviceCli::from_str(selection).unwrap() else {
4373 panic!("expected WinUSB discovery request");
4374 };
4375 assert_eq!(on.as_deref(), expected_pcie_port);
4376 }
4377 }
4378
4379 #[test]
4381 fn test_parse_usb_invalid_options() {
4382 for selection in [
4383 "",
4384 "unknown",
4385 "usb0:hid",
4386 "mock-hid,connector=0",
4387 "mock-hid,connector=256",
4388 "mock-hid,vid=0x10000",
4389 "mock-hid,pid=bad",
4390 "mock-hid,on=usb0,on=usb1",
4391 "mock-hid,blocks=1",
4392 "mock-hid,serial=",
4393 "mock-storage,blocks=0",
4394 "mock-supermutt,vid=1",
4395 "winusb",
4396 "winusb,instance_id=",
4397 "winusb,instance_id=USB\0INSTANCE",
4398 "winusb,instance_id=USB,serial=x",
4399 "winusb-all,connector=1",
4400 ] {
4401 assert!(
4402 UsbDeviceCli::from_str(selection).is_err(),
4403 "accepted {selection:?}"
4404 );
4405 }
4406 }
4407
4408 #[test]
4411 fn version_reports_build_info() {
4412 let short = version_output(["openvmm", "-V"]);
4413 assert_eq!(
4414 short,
4415 format!("openvmm {}\n", openvmm_build_info::get().version())
4416 );
4417
4418 let long = version_output(["openvmm", "--version"]);
4419 assert_eq!(
4420 long,
4421 format!("openvmm {}\n", openvmm_build_info::get().long_version())
4422 );
4423 }
4424
4425 fn version_output(args: [&str; 2]) -> String {
4426 let Err(error) = Options::try_parse_from(args) else {
4427 panic!("{args:?} unexpectedly parsed as runtime options");
4428 };
4429 assert_eq!(error.kind(), clap::error::ErrorKind::DisplayVersion);
4430 error.to_string()
4431 }
4432
4433 #[test]
4434 fn test_parse_rpc() {
4435 let rpc = RpcCli::from_str("path=/tmp/openvmm.sock").unwrap();
4437 assert_eq!(rpc.path, Path::new("/tmp/openvmm.sock"));
4438 assert_eq!(rpc.transport, RpcTransportCli::Auto);
4439
4440 for (s, transport) in [
4442 ("auto", RpcTransportCli::Auto),
4443 ("ttrpc", RpcTransportCli::Ttrpc),
4444 ("grpc", RpcTransportCli::Grpc),
4445 ] {
4446 let rpc = RpcCli::from_str(&format!("path=/tmp/s.sock,transport={s}")).unwrap();
4447 assert_eq!(rpc.path, Path::new("/tmp/s.sock"));
4448 assert_eq!(rpc.transport, transport);
4449 }
4450
4451 assert!(RpcCli::from_str("").is_err());
4453 assert!(RpcCli::from_str("transport=ttrpc").is_err());
4454 assert!(RpcCli::from_str("path=").is_err());
4455 assert!(RpcCli::from_str("path=/tmp/s.sock,transport=bogus").is_err());
4456 assert!(RpcCli::from_str("path=/tmp/s.sock,bogus=1").is_err());
4457 assert!(RpcCli::from_str("path=/a,path=/b").is_err());
4458 }
4459
4460 #[test]
4461 fn test_parse_file_opts() {
4462 let disk = DiskCliKind::from_str("file:test.vhd;create=1G").unwrap();
4464 assert!(matches!(
4465 &disk,
4466 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
4467 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
4468 ));
4469
4470 let disk = DiskCliKind::from_str("test.vhd;create=1G").unwrap();
4472 assert!(matches!(
4473 &disk,
4474 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
4475 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
4476 ));
4477
4478 let disk = DiskCliKind::from_str("file:/dev/sdb;direct").unwrap();
4480 assert!(matches!(
4481 &disk,
4482 DiskCliKind::File { path, create_with_len: None, direct: true }
4483 if path == Path::new("/dev/sdb")
4484 ));
4485
4486 let disk = DiskCliKind::from_str("file:disk.img;direct;create=1G").unwrap();
4488 assert!(matches!(
4489 &disk,
4490 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
4491 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
4492 ));
4493
4494 let disk = DiskCliKind::from_str("file:disk.img;create=1G;direct").unwrap();
4495 assert!(matches!(
4496 &disk,
4497 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
4498 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
4499 ));
4500
4501 let disk = DiskCliKind::from_str("file:disk.img").unwrap();
4503 assert!(matches!(
4504 &disk,
4505 DiskCliKind::File { path, create_with_len: None, direct: false }
4506 if path == Path::new("disk.img")
4507 ));
4508
4509 assert!(DiskCliKind::from_str("file:disk.img;bogus").is_err());
4511
4512 assert!(DiskCliKind::from_str("sql:db.sqlite;direct").is_err());
4514 }
4515
4516 #[test]
4517 fn test_parse_memory_disk() {
4518 let s = "mem:1G";
4519 let disk = DiskCliKind::from_str(s).unwrap();
4520 match disk {
4521 DiskCliKind::Memory(size) => {
4522 assert_eq!(size, 1024 * 1024 * 1024); }
4524 _ => panic!("Expected Memory variant"),
4525 }
4526 }
4527
4528 #[test]
4529 fn test_parse_pcie_disk() {
4530 assert_eq!(
4531 DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
4532 Some("p0".to_string())
4533 );
4534 assert_eq!(
4535 DiskCli::from_str("file:path.vhdx,pcie_port=p0")
4536 .unwrap()
4537 .pcie_port,
4538 Some("p0".to_string())
4539 );
4540 assert_eq!(
4541 DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
4542 .unwrap()
4543 .pcie_port,
4544 Some("p0".to_string())
4545 );
4546
4547 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
4549
4550 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
4552 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
4553 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
4554 }
4555
4556 #[test]
4557 fn test_parse_memory_diff_disk() {
4558 let s = "memdiff:file:base.img";
4559 let disk = DiskCliKind::from_str(s).unwrap();
4560 match disk {
4561 DiskCliKind::MemoryDiff(inner) => match *inner {
4562 DiskCliKind::File {
4563 path,
4564 create_with_len,
4565 ..
4566 } => {
4567 assert_eq!(path, PathBuf::from("base.img"));
4568 assert_eq!(create_with_len, None);
4569 }
4570 _ => panic!("Expected File variant inside MemoryDiff"),
4571 },
4572 _ => panic!("Expected MemoryDiff variant"),
4573 }
4574 }
4575
4576 #[test]
4577 fn test_parse_sqlite_disk() {
4578 let s = "sql:db.sqlite;create=2G";
4579 let disk = DiskCliKind::from_str(s).unwrap();
4580 match disk {
4581 DiskCliKind::Sqlite {
4582 path,
4583 create_with_len,
4584 } => {
4585 assert_eq!(path, PathBuf::from("db.sqlite"));
4586 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
4587 }
4588 _ => panic!("Expected Sqlite variant"),
4589 }
4590
4591 let s = "sql:db.sqlite";
4593 let disk = DiskCliKind::from_str(s).unwrap();
4594 match disk {
4595 DiskCliKind::Sqlite {
4596 path,
4597 create_with_len,
4598 } => {
4599 assert_eq!(path, PathBuf::from("db.sqlite"));
4600 assert_eq!(create_with_len, None);
4601 }
4602 _ => panic!("Expected Sqlite variant"),
4603 }
4604 }
4605
4606 #[test]
4607 fn test_parse_sqlite_diff_disk() {
4608 let s = "sqldiff:diff.sqlite;create:file:base.img";
4610 let disk = DiskCliKind::from_str(s).unwrap();
4611 match disk {
4612 DiskCliKind::SqliteDiff { path, create, disk } => {
4613 assert_eq!(path, PathBuf::from("diff.sqlite"));
4614 assert!(create);
4615 match *disk {
4616 DiskCliKind::File {
4617 path,
4618 create_with_len,
4619 ..
4620 } => {
4621 assert_eq!(path, PathBuf::from("base.img"));
4622 assert_eq!(create_with_len, None);
4623 }
4624 _ => panic!("Expected File variant inside SqliteDiff"),
4625 }
4626 }
4627 _ => panic!("Expected SqliteDiff variant"),
4628 }
4629
4630 let s = "sqldiff:diff.sqlite:file:base.img";
4632 let disk = DiskCliKind::from_str(s).unwrap();
4633 match disk {
4634 DiskCliKind::SqliteDiff { path, create, disk } => {
4635 assert_eq!(path, PathBuf::from("diff.sqlite"));
4636 assert!(!create);
4637 match *disk {
4638 DiskCliKind::File {
4639 path,
4640 create_with_len,
4641 ..
4642 } => {
4643 assert_eq!(path, PathBuf::from("base.img"));
4644 assert_eq!(create_with_len, None);
4645 }
4646 _ => panic!("Expected File variant inside SqliteDiff"),
4647 }
4648 }
4649 _ => panic!("Expected SqliteDiff variant"),
4650 }
4651 }
4652
4653 #[test]
4654 fn test_parse_autocache_sqlite_disk() {
4655 let disk =
4657 DiskCliKind::parse_autocache(":file:disk.vhd", Ok("/tmp/cache".to_string())).unwrap();
4658 assert!(matches!(
4659 disk,
4660 DiskCliKind::AutoCacheSqlite {
4661 cache_path,
4662 key,
4663 disk: _disk,
4664 } if cache_path == "/tmp/cache" && key.is_none()
4665 ));
4666
4667 let disk =
4669 DiskCliKind::parse_autocache("mykey:file:disk.vhd", Ok("/tmp/cache".to_string()))
4670 .unwrap();
4671 assert!(matches!(
4672 disk,
4673 DiskCliKind::AutoCacheSqlite {
4674 cache_path,
4675 key: Some(key),
4676 disk: _disk,
4677 } if cache_path == "/tmp/cache" && key == "mykey"
4678 ));
4679
4680 assert!(
4682 DiskCliKind::parse_autocache(":file:disk.vhd", Err(std::env::VarError::NotPresent),)
4683 .is_err()
4684 );
4685 }
4686
4687 #[test]
4688 fn test_parse_disk_errors() {
4689 assert!(DiskCliKind::from_str("invalid:").is_err());
4690 assert!(DiskCliKind::from_str("memory:extra").is_err());
4691
4692 assert!(DiskCliKind::from_str("sqlite:").is_err());
4694 }
4695
4696 #[test]
4697 fn test_parse_errors() {
4698 assert!(DiskCliKind::from_str("mem:invalid").is_err());
4700
4701 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
4703
4704 assert!(
4706 DiskCliKind::parse_autocache("key:file:disk.vhd", Err(std::env::VarError::NotPresent),)
4707 .is_err()
4708 );
4709
4710 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
4712
4713 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
4715
4716 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
4718
4719 assert!(DiskCliKind::from_str("invalid:path").is_err());
4721
4722 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
4724 }
4725
4726 #[test]
4727 fn test_fs_args_from_str() {
4728 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
4729 assert_eq!(args.tag, "tag1");
4730 assert_eq!(args.path, "/path/to/fs");
4731
4732 assert!(FsArgs::from_str("tag1").is_err());
4734 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
4735 }
4736
4737 #[test]
4738 fn test_fs_args_with_options_from_str() {
4739 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
4740 assert_eq!(args.tag, "tag1");
4741 assert_eq!(args.path, "/path/to/fs");
4742 assert_eq!(args.options, "opt1;opt2");
4743
4744 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
4746 assert_eq!(args.tag, "tag1");
4747 assert_eq!(args.path, "/path/to/fs");
4748 assert_eq!(args.options, "");
4749
4750 assert!(FsArgsWithOptions::from_str("tag1").is_err());
4752 }
4753
4754 #[test]
4755 fn test_serial_config_from_str() {
4756 assert_eq!(
4757 SerialConfigCli::from_str("none").unwrap(),
4758 SerialConfigCli::None
4759 );
4760 assert_eq!(
4761 SerialConfigCli::from_str("null").unwrap(),
4762 SerialConfigCli::Null
4763 );
4764 assert_eq!(
4765 SerialConfigCli::from_str("console").unwrap(),
4766 SerialConfigCli::Console
4767 );
4768 assert_eq!(
4769 SerialConfigCli::from_str("stderr").unwrap(),
4770 SerialConfigCli::Stderr
4771 );
4772
4773 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
4775 if let SerialConfigCli::File(path) = file_config {
4776 assert_eq!(path.to_str().unwrap(), "/path/to/file");
4777 } else {
4778 panic!("Expected File variant");
4779 }
4780
4781 match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
4783 SerialConfigCli::NewConsole(None, Some(name)) => {
4784 assert_eq!(name, "MyTerm");
4785 }
4786 _ => panic!("Expected NewConsole variant with name"),
4787 }
4788
4789 match SerialConfigCli::from_str("term").unwrap() {
4791 SerialConfigCli::NewConsole(None, None) => (),
4792 _ => panic!("Expected NewConsole variant without name"),
4793 }
4794
4795 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
4797 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
4798 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
4799 assert_eq!(name, "MyTerm");
4800 }
4801 _ => panic!("Expected NewConsole variant with name"),
4802 }
4803
4804 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
4806 SerialConfigCli::NewConsole(Some(path), None) => {
4807 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
4808 }
4809 _ => panic!("Expected NewConsole variant without name"),
4810 }
4811
4812 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
4814 SerialConfigCli::Tcp(addr) => {
4815 assert_eq!(addr.to_string(), "127.0.0.1:1234");
4816 }
4817 _ => panic!("Expected Tcp variant"),
4818 }
4819
4820 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
4822 SerialConfigCli::Pipe(path) => {
4823 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
4824 }
4825 _ => panic!("Expected Pipe variant"),
4826 }
4827
4828 assert!(SerialConfigCli::from_str("").is_err());
4830 assert!(SerialConfigCli::from_str("unknown").is_err());
4831 assert!(SerialConfigCli::from_str("file").is_err());
4832 assert!(SerialConfigCli::from_str("listen").is_err());
4833 assert!(SerialConfigCli::from_str("listen=tcp:").is_err());
4834 assert!(SerialConfigCli::from_str("listen=tcp:invalid").is_err());
4835 }
4836
4837 #[test]
4838 fn test_endpoint_config_from_str() {
4839 assert!(matches!(
4841 EndpointConfigCli::from_str("none").unwrap(),
4842 EndpointConfigCli::None
4843 ));
4844
4845 match EndpointConfigCli::from_str("consomme").unwrap() {
4847 EndpointConfigCli::Consomme {
4848 cidr: None,
4849 host_fwd,
4850 } => assert!(host_fwd.is_empty()),
4851 _ => panic!("Expected Consomme variant without cidr"),
4852 }
4853
4854 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
4856 EndpointConfigCli::Consomme {
4857 cidr: Some(cidr),
4858 host_fwd,
4859 } => {
4860 assert_eq!(cidr, "192.168.0.0/24");
4861 assert!(host_fwd.is_empty());
4862 }
4863 _ => panic!("Expected Consomme variant with cidr"),
4864 }
4865
4866 match EndpointConfigCli::from_str("consomme:hostfwd=udp:127.0.0.1:5000-:5000").unwrap() {
4868 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4869 assert!(cidr.is_none());
4870 assert_eq!(host_fwd.len(), 1);
4871 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Udp);
4872 assert_eq!(
4873 host_fwd[0].host_address,
4874 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
4875 );
4876 assert_eq!(host_fwd[0].host_port, 5000);
4877 assert_eq!(host_fwd[0].guest_port, 5000);
4878 }
4879 _ => panic!("Expected Consomme variant with hostfwd"),
4880 }
4881
4882 match EndpointConfigCli::from_str("consomme:10.0.0.0/24,hostfwd=tcp::2222-:22").unwrap() {
4884 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4885 assert_eq!(cidr.as_deref(), Some("10.0.0.0/24"));
4886 assert_eq!(host_fwd.len(), 1);
4887 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4888 assert_eq!(host_fwd[0].host_port, 2222);
4889 assert_eq!(host_fwd[0].guest_port, 22);
4890 }
4891 _ => panic!("Expected Consomme variant with cidr and hostfwd"),
4892 }
4893
4894 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::2222-:22,hostfwd=tcp::3389-:3389")
4896 .unwrap()
4897 {
4898 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4899 assert!(cidr.is_none());
4900 assert_eq!(host_fwd.len(), 2);
4901 assert_eq!(host_fwd[0].host_port, 2222);
4902 assert_eq!(host_fwd[0].guest_port, 22);
4903 assert_eq!(host_fwd[1].host_port, 3389);
4904 assert_eq!(host_fwd[1].guest_port, 3389);
4905 }
4906 _ => panic!("Expected Consomme variant with multiple hostfwd"),
4907 }
4908
4909 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:127.0.0.1:8080-:80").unwrap() {
4911 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4912 assert!(cidr.is_none());
4913 assert_eq!(host_fwd.len(), 1);
4914 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4915 assert_eq!(
4916 host_fwd[0].host_address,
4917 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
4918 );
4919 assert_eq!(host_fwd[0].host_port, 8080);
4920 assert_eq!(host_fwd[0].guest_port, 80);
4921 }
4922 _ => panic!("Expected Consomme variant with host/guest port mapping"),
4923 }
4924
4925 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-10.0.0.2:80").unwrap() {
4927 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4928 assert!(cidr.is_none());
4929 assert_eq!(host_fwd[0].host_port, 8080);
4930 assert_eq!(host_fwd[0].guest_port, 80);
4931 }
4932 _ => panic!("Expected Consomme variant with guest address"),
4933 }
4934
4935 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:[::1]:8080-:80").unwrap() {
4937 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4938 assert!(cidr.is_none());
4939 assert_eq!(host_fwd.len(), 1);
4940 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4941 assert_eq!(
4942 host_fwd[0].host_address,
4943 Some(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST))
4944 );
4945 assert_eq!(host_fwd[0].host_port, 8080);
4946 assert_eq!(host_fwd[0].guest_port, 80);
4947 }
4948 _ => panic!("Expected Consomme variant with IPv6 hostfwd"),
4949 }
4950
4951 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-[::1]:80").unwrap() {
4953 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4954 assert!(cidr.is_none());
4955 assert_eq!(host_fwd[0].host_port, 8080);
4956 assert_eq!(host_fwd[0].guest_port, 80);
4957 }
4958 _ => panic!("Expected Consomme variant with IPv6 guest address"),
4959 }
4960
4961 match EndpointConfigCli::from_str("dio").unwrap() {
4963 EndpointConfigCli::Dio { id: None } => (),
4964 _ => panic!("Expected Dio variant without id"),
4965 }
4966
4967 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
4969 EndpointConfigCli::Dio { id: Some(id) } => {
4970 assert_eq!(id, "test_id");
4971 }
4972 _ => panic!("Expected Dio variant with id"),
4973 }
4974
4975 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
4977 EndpointConfigCli::Tap { name } => {
4978 assert_eq!(name, "tap0");
4979 }
4980 _ => panic!("Expected Tap variant"),
4981 }
4982
4983 assert!(EndpointConfigCli::from_str("invalid").is_err());
4985 }
4986
4987 #[test]
4988 fn test_nic_config_from_str() {
4989 use openvmm_defs::config::DeviceVtl;
4990
4991 let config = NicConfigCli::from_str("none").unwrap();
4993 assert_eq!(config.vtl, DeviceVtl::Vtl0);
4994 assert!(config.mac_address.is_none());
4995 assert!(config.max_queues.is_none());
4996 assert!(!config.enable_rx_gso);
4997 assert!(!config.underhill);
4998 assert!(config.pcie_port.is_none());
4999 assert!(matches!(config.endpoint, EndpointConfigCli::None));
5000
5001 let config = NicConfigCli::from_str("rx_gso:tap:tap0").unwrap();
5002 assert!(config.enable_rx_gso);
5003 assert!(matches!(
5004 config.endpoint,
5005 EndpointConfigCli::Tap { ref name } if name == "tap0"
5006 ));
5007
5008 let config = NicConfigCli::from_str("vtl2:none").unwrap();
5010 assert_eq!(config.vtl, DeviceVtl::Vtl2);
5011 assert!(config.pcie_port.is_none());
5012 assert!(matches!(config.endpoint, EndpointConfigCli::None));
5013
5014 let config = NicConfigCli::from_str("queues=4:none").unwrap();
5016 assert_eq!(config.max_queues, Some(4));
5017 assert!(config.pcie_port.is_none());
5018 assert!(matches!(config.endpoint, EndpointConfigCli::None));
5019
5020 let config = NicConfigCli::from_str("mac=00-15-5d-12-12-13:none").unwrap();
5022 assert_eq!(
5023 config.mac_address,
5024 Some(MacAddress::new([0x00, 0x15, 0x5d, 0x12, 0x12, 0x13]))
5025 );
5026 assert!(matches!(config.endpoint, EndpointConfigCli::None));
5027
5028 let config = NicConfigCli::from_str("uh:none").unwrap();
5030 assert!(config.underhill);
5031 assert!(config.pcie_port.is_none());
5032 assert!(matches!(config.endpoint, EndpointConfigCli::None));
5033
5034 let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
5036 assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
5037 assert!(matches!(config.endpoint, EndpointConfigCli::None));
5038
5039 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
5041 assert!(NicConfigCli::from_str("mac=invalid:none").is_err());
5042 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
5044 assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
5045 assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
5046 assert!(NicConfigCli::from_str("pcie_port:none").is_err());
5047 }
5048
5049 #[test]
5050 fn test_parse_pcie_port_prefix() {
5051 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
5053 assert_eq!(port.unwrap(), "rp0");
5054 assert_eq!(rest, "tag,path");
5055
5056 let (port, rest) = parse_pcie_port_prefix("tag,path");
5058 assert!(port.is_none());
5059 assert_eq!(rest, "tag,path");
5060
5061 let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
5063 assert!(port.is_none());
5064 assert_eq!(rest, "pcie_port=:tag,path");
5065
5066 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
5068 assert!(port.is_none());
5069 assert_eq!(rest, "pcie_port=rp0");
5070 }
5071
5072 #[test]
5073 fn test_cxl_test_device_cli_parse_valid() {
5074 let cfg = CxlTestDeviceCli::from_str("mem:1G,pcie_port=rp0").unwrap();
5075 assert_eq!(cfg.hdm_size, 1024 * 1024 * 1024);
5076 assert_eq!(cfg.pcie_port, "rp0");
5077 }
5078
5079 #[test]
5080 fn test_cxl_test_device_cli_parse_invalid() {
5081 assert!(CxlTestDeviceCli::from_str("file:disk.img,pcie_port=rp0").is_err());
5082 assert!(CxlTestDeviceCli::from_str("mem:1G").is_err());
5083 assert!(CxlTestDeviceCli::from_str("mem:1G,pcie_port=").is_err());
5084 }
5085
5086 #[test]
5087 fn test_fs_args_pcie_port() {
5088 let args = FsArgs::from_str("myfs,/path").unwrap();
5090 assert_eq!(args.tag, "myfs");
5091 assert_eq!(args.path, "/path");
5092 assert!(args.pcie_port.is_none());
5093
5094 let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
5096 assert_eq!(args.pcie_port.unwrap(), "rp0");
5097 assert_eq!(args.tag, "myfs");
5098 assert_eq!(args.path, "/path");
5099
5100 assert!(FsArgs::from_str("myfs").is_err());
5102 assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
5103 }
5104
5105 #[test]
5106 fn test_fs_args_with_options_pcie_port() {
5107 let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
5109 assert_eq!(args.tag, "myfs");
5110 assert_eq!(args.path, "/path");
5111 assert_eq!(args.options, "uid=1000");
5112 assert!(args.pcie_port.is_none());
5113
5114 let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
5116 assert_eq!(args.pcie_port.unwrap(), "rp0");
5117 assert_eq!(args.tag, "myfs");
5118 assert_eq!(args.path, "/path");
5119 assert_eq!(args.options, "uid=1000");
5120
5121 assert!(FsArgsWithOptions::from_str("myfs").is_err());
5123 }
5124
5125 #[test]
5126 fn test_virtio_pmem_args_pcie_port() {
5127 let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
5129 assert_eq!(args.path, "/path/to/file");
5130 assert!(args.pcie_port.is_none());
5131
5132 let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
5134 assert_eq!(args.pcie_port.unwrap(), "rp0");
5135 assert_eq!(args.path, "/path/to/file");
5136
5137 assert!(VirtioPmemArgs::from_str("").is_err());
5139 assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
5140 }
5141
5142 #[test]
5143 fn test_smt_config_from_str() {
5144 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
5145 assert_eq!(
5146 SmtConfigCli::from_str("force").unwrap(),
5147 SmtConfigCli::Force
5148 );
5149 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
5150
5151 assert!(SmtConfigCli::from_str("invalid").is_err());
5153 assert!(SmtConfigCli::from_str("").is_err());
5154 }
5155
5156 #[test]
5157 fn test_pcat_boot_order_from_str() {
5158 let order = PcatBootOrderCli::from_str("optical").unwrap();
5160 assert_eq!(order.0[0], PcatBootDevice::Optical);
5161
5162 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
5164 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
5165 assert_eq!(order.0[1], PcatBootDevice::Network);
5166
5167 assert!(PcatBootOrderCli::from_str("invalid").is_err());
5169 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
5171
5172 #[test]
5173 fn test_floppy_disk_from_str() {
5174 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
5176 assert!(!disk.read_only);
5177 match disk.kind {
5178 DiskCliKind::File {
5179 path,
5180 create_with_len,
5181 ..
5182 } => {
5183 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
5184 assert_eq!(create_with_len, None);
5185 }
5186 _ => panic!("Expected File variant"),
5187 }
5188
5189 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
5191 assert!(disk.read_only);
5192
5193 assert!(FloppyDiskCli::from_str("").is_err());
5195 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
5196 }
5197
5198 #[test]
5199 fn test_pcie_root_complex_from_str() {
5200 const ONE_MB: u64 = 1024 * 1024;
5201 const ONE_GB: u64 = 1024 * ONE_MB;
5202
5203 const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
5204 const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
5205 const DEFAULT_HDM: u64 = ONE_GB;
5206 const DEFAULT_HDM_WINDOW_RESTRICTIONS: CfmwsWindowRestrictions =
5207 CfmwsWindowRestrictions::DEVICE_COHERENT;
5208
5209 assert_eq!(
5210 PcieRootComplexCli::from_str("rc0").unwrap(),
5211 PcieRootComplexCli {
5212 name: "rc0".to_string(),
5213 segment: 0,
5214 start_bus: 0,
5215 end_bus: 255,
5216 low_mmio: DEFAULT_LOW_MMIO,
5217 high_mmio: DEFAULT_HIGH_MMIO,
5218 hdm: DEFAULT_HDM,
5219 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
5220 vnode: None,
5221 low_mmio_base: None,
5222 high_mmio_base: None,
5223 preserve_bars: false,
5224 }
5225 );
5226
5227 assert_eq!(
5228 PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
5229 PcieRootComplexCli {
5230 name: "rc1".to_string(),
5231 segment: 1,
5232 start_bus: 0,
5233 end_bus: 255,
5234 low_mmio: DEFAULT_LOW_MMIO,
5235 high_mmio: DEFAULT_HIGH_MMIO,
5236 hdm: DEFAULT_HDM,
5237 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
5238 vnode: None,
5239 low_mmio_base: None,
5240 high_mmio_base: None,
5241 preserve_bars: false,
5242 }
5243 );
5244
5245 assert_eq!(
5246 PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
5247 PcieRootComplexCli {
5248 name: "rc2".to_string(),
5249 segment: 0,
5250 start_bus: 32,
5251 end_bus: 255,
5252 low_mmio: DEFAULT_LOW_MMIO,
5253 high_mmio: DEFAULT_HIGH_MMIO,
5254 hdm: DEFAULT_HDM,
5255 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
5256 vnode: None,
5257 low_mmio_base: None,
5258 high_mmio_base: None,
5259 preserve_bars: false,
5260 }
5261 );
5262
5263 assert_eq!(
5264 PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
5265 PcieRootComplexCli {
5266 name: "rc3".to_string(),
5267 segment: 0,
5268 start_bus: 0,
5269 end_bus: 31,
5270 low_mmio: DEFAULT_LOW_MMIO,
5271 high_mmio: DEFAULT_HIGH_MMIO,
5272 hdm: DEFAULT_HDM,
5273 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
5274 vnode: None,
5275 low_mmio_base: None,
5276 high_mmio_base: None,
5277 preserve_bars: false,
5278 }
5279 );
5280
5281 assert_eq!(
5282 PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
5283 PcieRootComplexCli {
5284 name: "rc4".to_string(),
5285 segment: 0,
5286 start_bus: 32,
5287 end_bus: 127,
5288 low_mmio: DEFAULT_LOW_MMIO,
5289 high_mmio: 2 * ONE_GB,
5290 hdm: DEFAULT_HDM,
5291 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
5292 vnode: None,
5293 low_mmio_base: None,
5294 high_mmio_base: None,
5295 preserve_bars: false,
5296 }
5297 );
5298
5299 assert_eq!(
5300 PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
5301 PcieRootComplexCli {
5302 name: "rc5".to_string(),
5303 segment: 2,
5304 start_bus: 32,
5305 end_bus: 127,
5306 low_mmio: DEFAULT_LOW_MMIO,
5307 high_mmio: DEFAULT_HIGH_MMIO,
5308 hdm: DEFAULT_HDM,
5309 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
5310 vnode: None,
5311 low_mmio_base: None,
5312 high_mmio_base: None,
5313 preserve_bars: false,
5314 }
5315 );
5316
5317 assert_eq!(
5318 PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
5319 PcieRootComplexCli {
5320 name: "rc6".to_string(),
5321 segment: 0,
5322 start_bus: 0,
5323 end_bus: 255,
5324 low_mmio: ONE_MB as u32,
5325 high_mmio: 64 * ONE_GB,
5326 hdm: DEFAULT_HDM,
5327 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
5328 vnode: None,
5329 low_mmio_base: None,
5330 high_mmio_base: None,
5331 preserve_bars: false,
5332 }
5333 );
5334
5335 assert_eq!(
5336 PcieRootComplexCli::from_str("rc7,hdm=2G").unwrap(),
5337 PcieRootComplexCli {
5338 name: "rc7".to_string(),
5339 segment: 0,
5340 start_bus: 0,
5341 end_bus: 255,
5342 low_mmio: DEFAULT_LOW_MMIO,
5343 high_mmio: DEFAULT_HIGH_MMIO,
5344 hdm: 2 * ONE_GB,
5345 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
5346 vnode: None,
5347 low_mmio_base: None,
5348 high_mmio_base: None,
5349 preserve_bars: false,
5350 }
5351 );
5352
5353 assert_eq!(
5354 PcieRootComplexCli::from_str("rc8,hdm_window_restrictions=0x21").unwrap(),
5355 PcieRootComplexCli {
5356 name: "rc8".to_string(),
5357 segment: 0,
5358 start_bus: 0,
5359 end_bus: 255,
5360 low_mmio: DEFAULT_LOW_MMIO,
5361 high_mmio: DEFAULT_HIGH_MMIO,
5362 hdm: DEFAULT_HDM,
5363 hdm_window_restrictions: CfmwsWindowRestrictions::try_from_bits(0x21).unwrap(),
5364 vnode: None,
5365 low_mmio_base: None,
5366 high_mmio_base: None,
5367 preserve_bars: false,
5368 }
5369 );
5370
5371 assert!(PcieRootComplexCli::from_str("").is_err());
5373 assert!(PcieRootComplexCli::from_str("poorly,").is_err());
5374 assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
5375 assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
5376 assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
5377 assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
5378 assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
5379 assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
5380 assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
5381 assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
5382 assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
5383 assert!(PcieRootComplexCli::from_str("rc,hdm=bad").is_err());
5384 assert!(PcieRootComplexCli::from_str("rc,hdm").is_err());
5385 assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions=bad").is_err());
5386 assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions").is_err());
5387 assert!(PcieRootComplexCli::from_str("rc,cxl").is_err());
5388
5389 assert_eq!(
5391 PcieRootComplexCli::from_str("rc9,node=1").unwrap(),
5392 PcieRootComplexCli {
5393 name: "rc9".to_string(),
5394 segment: 0,
5395 start_bus: 0,
5396 end_bus: 255,
5397 low_mmio: DEFAULT_LOW_MMIO,
5398 high_mmio: DEFAULT_HIGH_MMIO,
5399 hdm: DEFAULT_HDM,
5400 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
5401 vnode: Some(1),
5402 low_mmio_base: None,
5403 high_mmio_base: None,
5404 preserve_bars: false,
5405 }
5406 );
5407 }
5408
5409 #[test]
5410 fn test_pcie_root_port_from_str() {
5411 assert_eq!(
5412 PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
5413 PcieRootPortCli {
5414 root_complex_name: "rc0".to_string(),
5415 name: "rc0rp0".to_string(),
5416 devfn: None,
5417 hotplug: false,
5418 acs_capabilities_supported: None,
5419 cxl: false,
5420 pasid: false,
5421 }
5422 );
5423
5424 assert_eq!(
5425 PcieRootPortCli::from_str("my_rc:port2").unwrap(),
5426 PcieRootPortCli {
5427 root_complex_name: "my_rc".to_string(),
5428 name: "port2".to_string(),
5429 devfn: None,
5430 hotplug: false,
5431 acs_capabilities_supported: None,
5432 cxl: false,
5433 pasid: false,
5434 }
5435 );
5436
5437 assert_eq!(
5439 PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
5440 PcieRootPortCli {
5441 root_complex_name: "my_rc".to_string(),
5442 name: "port2".to_string(),
5443 devfn: None,
5444 hotplug: true,
5445 acs_capabilities_supported: None,
5446 cxl: false,
5447 pasid: false,
5448 }
5449 );
5450
5451 assert_eq!(
5452 PcieRootPortCli::from_str("my_rc:port3,acs=0").unwrap(),
5453 PcieRootPortCli {
5454 root_complex_name: "my_rc".to_string(),
5455 name: "port3".to_string(),
5456 devfn: None,
5457 hotplug: false,
5458 acs_capabilities_supported: Some(0),
5459 cxl: false,
5460 pasid: false,
5461 }
5462 );
5463
5464 assert_eq!(
5465 PcieRootPortCli::from_str("my_rc:port3,acs=0x5f").unwrap(),
5466 PcieRootPortCli {
5467 root_complex_name: "my_rc".to_string(),
5468 name: "port3".to_string(),
5469 devfn: None,
5470 hotplug: false,
5471 acs_capabilities_supported: Some(0x005f),
5472 cxl: false,
5473 pasid: false,
5474 }
5475 );
5476
5477 assert_eq!(
5478 PcieRootPortCli::from_str("my_rc:port4,cxl").unwrap(),
5479 PcieRootPortCli {
5480 root_complex_name: "my_rc".to_string(),
5481 name: "port4".to_string(),
5482 devfn: None,
5483 hotplug: false,
5484 acs_capabilities_supported: None,
5485 cxl: true,
5486 pasid: false,
5487 }
5488 );
5489
5490 assert_eq!(
5492 PcieRootPortCli::from_str("my_rc:port5,addr=5").unwrap(),
5493 PcieRootPortCli {
5494 root_complex_name: "my_rc".to_string(),
5495 name: "port5".to_string(),
5496 devfn: Some(5 << 3),
5497 hotplug: false,
5498 acs_capabilities_supported: None,
5499 cxl: false,
5500 pasid: false,
5501 }
5502 );
5503 assert_eq!(
5504 PcieRootPortCli::from_str("my_rc:port6,addr=5.1").unwrap(),
5505 PcieRootPortCli {
5506 root_complex_name: "my_rc".to_string(),
5507 name: "port6".to_string(),
5508 devfn: Some((5 << 3) | 1),
5509 hotplug: false,
5510 acs_capabilities_supported: None,
5511 cxl: false,
5512 pasid: false,
5513 }
5514 );
5515 assert_eq!(
5516 PcieRootPortCli::from_str("my_rc:port7,addr=0x1f.7").unwrap(),
5517 PcieRootPortCli {
5518 root_complex_name: "my_rc".to_string(),
5519 name: "port7".to_string(),
5520 devfn: Some(0xff),
5521 hotplug: false,
5522 acs_capabilities_supported: None,
5523 cxl: false,
5524 pasid: false,
5525 }
5526 );
5527
5528 assert_eq!(
5529 PcieRootPortCli::from_str("my_rc:port8,pasid").unwrap(),
5530 PcieRootPortCli {
5531 root_complex_name: "my_rc".to_string(),
5532 name: "port8".to_string(),
5533 devfn: None,
5534 hotplug: false,
5535 acs_capabilities_supported: None,
5536 cxl: false,
5537 pasid: true,
5538 }
5539 );
5540
5541 assert!(PcieRootPortCli::from_str("").is_err());
5543 assert!(PcieRootPortCli::from_str("rp0").is_err());
5544 assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
5545 assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
5546 assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
5547 assert!(PcieRootPortCli::from_str("rc0:rp0,cxl=true").is_err());
5548 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=32").is_err());
5549 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=0.8").is_err());
5550 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=1.2.3").is_err());
5551 assert!(PcieRootPortCli::from_str("rc0:rp0,addr").is_err());
5552 assert!(PcieRootPortCli::from_str("rc0:rp0,pasid=foo").is_err());
5553 }
5554
5555 #[test]
5556 fn test_pcie_generic_initiator_from_str() {
5557 assert_eq!(
5558 PcieGenericInitiatorCli::from_str("port=rp0,node=1").unwrap(),
5559 PcieGenericInitiatorCli {
5560 port_name: "rp0".to_string(),
5561 node: 1,
5562 }
5563 );
5564
5565 assert_eq!(
5567 PcieGenericInitiatorCli::from_str("node=2,port=sw0-downstream-1").unwrap(),
5568 PcieGenericInitiatorCli {
5569 port_name: "sw0-downstream-1".to_string(),
5570 node: 2,
5571 }
5572 );
5573
5574 assert!(PcieGenericInitiatorCli::from_str("").is_err());
5576 assert!(PcieGenericInitiatorCli::from_str("port=rp0").is_err());
5577 assert!(PcieGenericInitiatorCli::from_str("node=1").is_err());
5578 assert!(PcieGenericInitiatorCli::from_str("rp0=1").is_err());
5579 assert!(PcieGenericInitiatorCli::from_str("port=,node=1").is_err());
5580 assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=x").is_err());
5581 assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=1,extra").is_err());
5582 }
5583
5584 #[test]
5585 fn test_pcie_switch_from_str() {
5586 assert_eq!(
5587 GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
5588 GenericPcieSwitchCli {
5589 port_name: "rp0".to_string(),
5590 name: "switch0".to_string(),
5591 num_downstream_ports: 4,
5592 hotplug: false,
5593 acs_capabilities_supported: None,
5594 pasid: false,
5595 }
5596 );
5597
5598 assert_eq!(
5599 GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
5600 GenericPcieSwitchCli {
5601 port_name: "port1".to_string(),
5602 name: "my_switch".to_string(),
5603 num_downstream_ports: 4,
5604 hotplug: false,
5605 acs_capabilities_supported: None,
5606 pasid: false,
5607 }
5608 );
5609
5610 assert_eq!(
5611 GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
5612 GenericPcieSwitchCli {
5613 port_name: "rp2".to_string(),
5614 name: "sw".to_string(),
5615 num_downstream_ports: 8,
5616 hotplug: false,
5617 acs_capabilities_supported: None,
5618 pasid: false,
5619 }
5620 );
5621
5622 assert_eq!(
5624 GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
5625 GenericPcieSwitchCli {
5626 port_name: "switch0-downstream-1".to_string(),
5627 name: "child_switch".to_string(),
5628 num_downstream_ports: 4,
5629 hotplug: false,
5630 acs_capabilities_supported: None,
5631 pasid: false,
5632 }
5633 );
5634
5635 assert_eq!(
5637 GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
5638 GenericPcieSwitchCli {
5639 port_name: "rp0".to_string(),
5640 name: "switch0".to_string(),
5641 num_downstream_ports: 4,
5642 hotplug: true,
5643 acs_capabilities_supported: None,
5644 pasid: false,
5645 }
5646 );
5647
5648 assert_eq!(
5650 GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
5651 GenericPcieSwitchCli {
5652 port_name: "rp0".to_string(),
5653 name: "switch0".to_string(),
5654 num_downstream_ports: 8,
5655 hotplug: true,
5656 acs_capabilities_supported: None,
5657 pasid: false,
5658 }
5659 );
5660
5661 assert_eq!(
5662 GenericPcieSwitchCli::from_str("rp0:switch0,acs=0").unwrap(),
5663 GenericPcieSwitchCli {
5664 port_name: "rp0".to_string(),
5665 name: "switch0".to_string(),
5666 num_downstream_ports: 4,
5667 hotplug: false,
5668 acs_capabilities_supported: Some(0),
5669 pasid: false,
5670 }
5671 );
5672
5673 assert_eq!(
5674 GenericPcieSwitchCli::from_str("rp0:switch0,acs=95").unwrap(),
5675 GenericPcieSwitchCli {
5676 port_name: "rp0".to_string(),
5677 name: "switch0".to_string(),
5678 num_downstream_ports: 4,
5679 hotplug: false,
5680 acs_capabilities_supported: Some(95),
5681 pasid: false,
5682 }
5683 );
5684
5685 assert_eq!(
5686 GenericPcieSwitchCli::from_str("rp0:switch0,pasid").unwrap(),
5687 GenericPcieSwitchCli {
5688 port_name: "rp0".to_string(),
5689 name: "switch0".to_string(),
5690 num_downstream_ports: 4,
5691 hotplug: false,
5692 acs_capabilities_supported: None,
5693 pasid: true,
5694 }
5695 );
5696
5697 assert!(GenericPcieSwitchCli::from_str("").is_err());
5699 assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
5700 assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
5701 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
5702 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
5703 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
5704 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
5705 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,pasid=bar").is_err());
5706 }
5707
5708 #[test]
5709 fn test_pcie_remote_from_str() {
5710 assert_eq!(
5712 PcieRemoteCli::from_str("rc0rp0").unwrap(),
5713 PcieRemoteCli {
5714 port_name: "rc0rp0".to_string(),
5715 socket_addr: None,
5716 hu: 0,
5717 controller: 0,
5718 }
5719 );
5720
5721 assert_eq!(
5723 PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
5724 PcieRemoteCli {
5725 port_name: "rc0rp0".to_string(),
5726 socket_addr: Some("localhost:22567".to_string()),
5727 hu: 0,
5728 controller: 0,
5729 }
5730 );
5731
5732 assert_eq!(
5734 PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
5735 PcieRemoteCli {
5736 port_name: "myport".to_string(),
5737 socket_addr: Some("localhost:22568".to_string()),
5738 hu: 1,
5739 controller: 2,
5740 }
5741 );
5742
5743 assert_eq!(
5745 PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
5746 PcieRemoteCli {
5747 port_name: "port0".to_string(),
5748 socket_addr: None,
5749 hu: 5,
5750 controller: 3,
5751 }
5752 );
5753
5754 assert!(PcieRemoteCli::from_str("").is_err());
5756 assert!(PcieRemoteCli::from_str("port,socket=").is_err());
5757 assert!(PcieRemoteCli::from_str("port,hu=").is_err());
5758 assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
5759 assert!(PcieRemoteCli::from_str("port,controller=").is_err());
5760 assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
5761 assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
5762 }
5763
5764 #[test]
5765 fn test_parse_memory_units() {
5766 assert_eq!(parse_memory("64G").unwrap(), 64 * 1024 * 1024 * 1024);
5767 assert_eq!(parse_memory("64GB").unwrap(), 64 * 1024 * 1024 * 1024);
5768 assert_eq!(parse_memory("3MB").unwrap(), 3 * 1024 * 1024);
5769 assert_eq!(parse_memory("512KB").unwrap(), 512 * 1024);
5770 assert!(parse_memory("3MiB").is_err());
5771 }
5772
5773 #[test]
5774 fn test_memory_config_size_only() {
5775 assert_eq!(
5776 parse_memory_config("64G").unwrap(),
5777 MemoryCli {
5778 size: Some(vmm_cli::MemorySize(64 * 1024 * 1024 * 1024)),
5779 ..Default::default()
5780 }
5781 );
5782 }
5783
5784 #[test]
5785 fn test_memory_config_key_value() {
5786 assert_eq!(
5787 parse_memory_config("size=2G,shared=off,prefetch=on,thp=on").unwrap(),
5788 MemoryCli {
5789 size: Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024)),
5790 shared: Some(false),
5791 prefetch: true,
5792 transparent_hugepages: Some(true),
5793 ..Default::default()
5794 }
5795 );
5796
5797 assert_eq!(
5798 parse_memory_config("size=4GB,hugepages=on,hugepage_size=2MB").unwrap(),
5799 MemoryCli {
5800 size: Some(vmm_cli::MemorySize(4 * 1024 * 1024 * 1024)),
5801 hugepages: true,
5802 hugepage_size: Some(vmm_cli::MemorySize(2 * 1024 * 1024)),
5803 ..Default::default()
5804 }
5805 );
5806
5807 assert_eq!(
5808 parse_memory_config("file=/tmp/memory.bin").unwrap(),
5809 MemoryCli {
5810 file: Some(PathBuf::from("/tmp/memory.bin")),
5811 ..Default::default()
5812 }
5813 );
5814 }
5815
5816 #[test]
5817 fn test_memory_config_rejects_invalid_combinations() {
5818 assert!(parse_memory_config("size=1G,size=2G").is_err());
5819 assert!(parse_memory_config("hugepage_size=2M").is_err());
5820 assert!(parse_memory_config("hugepages=on,shared=off").is_err());
5821 assert!(parse_memory_config("hugepages=on,file=/tmp/memory.bin").is_err());
5822
5823 assert_eq!(
5826 parse_memory_config("hugepages=on,hugepage_size=3MB")
5827 .unwrap()
5828 .hugepage_size,
5829 Some(vmm_cli::MemorySize(3 * 1024 * 1024))
5830 );
5831 }
5832
5833 #[test]
5834 fn test_smbios_requires_type() {
5835 assert!(parse_smbios("manufacturer=Contoso,family=Foo").is_err());
5837 }
5838
5839 #[test]
5840 fn test_smbios_all_system_keys() {
5841 let parsed = parse_smbios(
5842 "type=1,manufacturer=M,product=P,version=V,serial=S,sku=K,family=F,\
5843 uuid=12345678-9abc-def0-1234-56789abcdef0",
5844 )
5845 .unwrap();
5846 assert_eq!(
5847 parsed,
5848 SmbiosCli {
5849 system: SmbiosSystemCli {
5850 manufacturer: Some("M".to_string()),
5851 product_name: Some("P".to_string()),
5852 version: Some("V".to_string()),
5853 serial_number: Some("S".to_string()),
5854 sku_number: Some("K".to_string()),
5855 family: Some("F".to_string()),
5856 uuid: Some(SmbiosUuid::Fixed(
5857 "12345678-9abc-def0-1234-56789abcdef0"
5858 .parse::<Guid>()
5859 .unwrap()
5860 )),
5861 },
5862 ..Default::default()
5863 }
5864 );
5865 }
5866
5867 #[test]
5868 fn test_smbios_bios_keys() {
5869 let parsed =
5870 parse_smbios("type=0,vendor=Contoso,version=1.0,date=01/01/2026,release=4.1").unwrap();
5871 assert_eq!(
5872 parsed,
5873 SmbiosCli {
5874 bios: SmbiosBiosCli {
5875 vendor: Some("Contoso".to_string()),
5876 version: Some("1.0".to_string()),
5877 release_date: Some("01/01/2026".to_string()),
5878 release: Some(SmbiosRelease(4, 1)),
5879 },
5880 ..Default::default()
5881 }
5882 );
5883 }
5884
5885 #[test]
5886 fn test_smbios_release_parsing() {
5887 assert_eq!(
5889 parse_smbios("type=0,release=4.1").unwrap().bios.release,
5890 Some(SmbiosRelease(4, 1))
5891 );
5892 assert_eq!(
5893 parse_smbios("type=0,release=255.0").unwrap().bios.release,
5894 Some(SmbiosRelease(255, 0))
5895 );
5896 assert!(parse_smbios("type=0,release=4").is_err());
5899 assert!(parse_smbios("type=0,release=4.1.0").is_err());
5900 assert!(parse_smbios("type=0,release=256.0").is_err());
5901 assert!(parse_smbios("type=0,release=x.y").is_err());
5902 assert!(parse_smbios("type=1,release=4.1").is_err());
5904 }
5905
5906 #[test]
5907 fn test_smbios_uuid() {
5908 assert_eq!(
5910 parse_smbios("type=1,uuid=12345678-9abc-def0-1234-56789abcdef0")
5911 .unwrap()
5912 .system
5913 .uuid,
5914 Some(SmbiosUuid::Fixed(
5915 "12345678-9abc-def0-1234-56789abcdef0"
5916 .parse::<Guid>()
5917 .unwrap()
5918 ))
5919 );
5920 assert_eq!(
5922 parse_smbios("type=1,uuid=random").unwrap().system.uuid,
5923 Some(SmbiosUuid::Random)
5924 );
5925 }
5926
5927 #[test]
5928 fn test_smbios_version_disambiguated_by_type() {
5929 let bios = parse_smbios("type=0,version=1.0").unwrap();
5931 assert_eq!(bios.bios.version.as_deref(), Some("1.0"));
5932 assert_eq!(bios.system.version, None);
5933
5934 let system = parse_smbios("type=1,version=2.0").unwrap();
5935 assert_eq!(system.system.version.as_deref(), Some("2.0"));
5936 assert_eq!(system.bios.version, None);
5937 }
5938
5939 #[test]
5940 fn test_smbios_rejects_invalid() {
5941 assert!(parse_smbios("manufacturer=M").is_err());
5943 assert!(parse_smbios("type=0,manufacturer=M").is_err());
5945 assert!(parse_smbios("type=1,nonsense=x").is_err());
5946 assert!(parse_smbios("type=2,vendor=M").is_err());
5948 assert!(parse_smbios("type=bad,vendor=M").is_err());
5949 assert!(parse_smbios("type=1,manufacturer").is_err());
5951 assert!(parse_smbios("=value").is_err());
5952 assert!(parse_smbios("type=1,manufacturer=").is_err());
5953 assert!(parse_smbios("type=1,manufacturer=A,manufacturer=B").is_err());
5955 assert!(parse_smbios("type=0,type=1,vendor=M").is_err());
5956 assert!(parse_smbios("type=1,uuid=not-a-guid").is_err());
5958 }
5959
5960 #[test]
5961 fn test_smbios_merge_across_arguments() {
5962 let mut merged = SmbiosCli::default();
5963 merged
5964 .merge(parse_smbios("type=0,vendor=Contoso").unwrap())
5965 .unwrap();
5966 merged
5967 .merge(parse_smbios("type=1,manufacturer=M,family=F").unwrap())
5968 .unwrap();
5969 assert_eq!(
5970 merged,
5971 SmbiosCli {
5972 bios: SmbiosBiosCli {
5973 vendor: Some("Contoso".to_string()),
5974 ..Default::default()
5975 },
5976 system: SmbiosSystemCli {
5977 manufacturer: Some("M".to_string()),
5978 family: Some("F".to_string()),
5979 ..Default::default()
5980 },
5981 }
5982 );
5983 }
5984
5985 #[test]
5986 fn test_smbios_merge_rejects_conflicts() {
5987 let mut merged = SmbiosCli::default();
5988 merged
5989 .merge(parse_smbios("type=1,manufacturer=A").unwrap())
5990 .unwrap();
5991 assert!(
5993 merged
5994 .merge(parse_smbios("type=1,manufacturer=B").unwrap())
5995 .is_err()
5996 );
5997 }
5998
5999 #[test]
6000 fn test_smbios_arg_repeatable() {
6001 let opt = Options::try_parse_from([
6002 "openvmm",
6003 "--smbios",
6004 "type=1,manufacturer=Contoso",
6005 "--smbios",
6006 "type=0,vendor=Acme",
6007 ])
6008 .unwrap();
6009 assert_eq!(opt.smbios.len(), 2);
6010 assert_eq!(
6011 opt.smbios[0].system.manufacturer.as_deref(),
6012 Some("Contoso")
6013 );
6014 assert_eq!(opt.smbios[1].bios.vendor.as_deref(), Some("Acme"));
6015 }
6016
6017 #[test]
6018 fn test_memory_options_merge_legacy_aliases() {
6019 let opt = Options::try_parse_from([
6020 "openvmm",
6021 "--memory",
6022 "2G",
6023 "--prefetch",
6024 "--private-memory",
6025 "--thp",
6026 ])
6027 .unwrap();
6028 opt.validate_memory_options().unwrap();
6029 assert_eq!(opt.memory_size(), 2 * 1024 * 1024 * 1024);
6030 assert!(opt.prefetch_memory());
6031 assert!(opt.private_memory());
6032 assert!(opt.transparent_hugepages());
6033 }
6034
6035 #[test]
6036 fn test_serial_debugger_mode_option_parsed() {
6037 let opt = Options::try_parse_from(["openvmm"]).unwrap();
6039 assert!(opt.com1.is_none());
6040
6041 let opt = Options::try_parse_from(["openvmm", "--com1", "none"]).unwrap();
6043 let com1 = opt.com1.unwrap();
6044 assert!(!com1.debugger_mode);
6045 assert_eq!(com1.backend, SerialConfigCli::None);
6046
6047 let opt = Options::try_parse_from([
6050 "openvmm",
6051 "--com1",
6052 "debugger-mode:listen=/tmp/kd",
6053 "--com2",
6054 "none",
6055 ])
6056 .unwrap();
6057 let com1 = opt.com1.unwrap();
6058 assert!(com1.debugger_mode);
6059 assert_eq!(com1.backend, SerialConfigCli::Pipe("/tmp/kd".into()));
6060 assert!(!opt.com2.unwrap().debugger_mode);
6062
6063 let opt = Options::try_parse_from([
6065 "openvmm",
6066 "--com1",
6067 "debugger-mode:listen=tcp:127.0.0.1:5555",
6068 ])
6069 .unwrap();
6070 let com1 = opt.com1.unwrap();
6071 assert!(com1.debugger_mode);
6072 assert_eq!(
6073 com1.backend,
6074 SerialConfigCli::Tcp("127.0.0.1:5555".parse().unwrap())
6075 );
6076 }
6077
6078 #[test]
6079 fn test_no_hv_requires_no_vmbus() {
6080 assert!(Options::try_parse_from(["openvmm", "--no-hv"]).is_err());
6081
6082 let opt = Options::try_parse_from(["openvmm", "--uefi", "--no-hv", "--no-vmbus"]).unwrap();
6083 assert!(opt.no_hv);
6084 assert!(opt.no_vmbus);
6085
6086 assert!(
6087 Options::try_parse_from(["openvmm", "--uefi", "--no-hv", "--no-vmbus", "--hv"])
6088 .is_err()
6089 );
6090 }
6091
6092 #[test]
6093 fn test_memory_options_allow_legacy_thp_with_new_private_memory() {
6094 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=off", "--thp"]).unwrap();
6095 opt.validate_memory_options().unwrap();
6096 assert!(opt.private_memory());
6097 assert!(opt.transparent_hugepages());
6098 }
6099
6100 #[test]
6101 fn test_memory_options_reject_conflicting_legacy_aliases() {
6102 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=on", "--private-memory"])
6103 .unwrap();
6104 assert!(opt.validate_memory_options().is_err());
6105 }
6106
6107 #[test]
6108 fn test_isolation_options_reject_snp_uefi() {
6109 let opt = Options::try_parse_from(["openvmm", "--isolation", "snp", "--uefi"]).unwrap();
6110
6111 assert_eq!(
6112 opt.validate_isolation_options().unwrap_err().to_string(),
6113 "SNP isolation currently only supports Linux direct boot"
6114 );
6115 }
6116
6117 #[test]
6118 fn test_isolation_options_reject_snp_hugepages() {
6119 for args in [
6120 vec![
6121 "openvmm",
6122 "--isolation",
6123 "snp",
6124 "--memory",
6125 "size=1G,hugepages=on",
6126 ],
6127 vec![
6128 "openvmm",
6129 "--isolation",
6130 "snp",
6131 "--numa",
6132 "size=1G,hugepages=on",
6133 ],
6134 ] {
6135 let opt = Options::try_parse_from(args).unwrap();
6136 assert_eq!(
6137 opt.validate_isolation_options().unwrap_err().to_string(),
6138 "SNP isolation currently does not support hugetlb memory"
6139 );
6140 }
6141 }
6142
6143 #[test]
6144 fn test_restricted_injection_requires_snp() {
6145 let opt = Options::try_parse_from(["openvmm", "--snp-restricted-injection"]).unwrap();
6146
6147 assert_eq!(
6148 opt.validate_isolation_options().unwrap_err().to_string(),
6149 "--snp-restricted-injection requires --isolation snp"
6150 );
6151
6152 let opt = Options::try_parse_from([
6153 "openvmm",
6154 "--isolation",
6155 "snp",
6156 "--snp-restricted-injection",
6157 ])
6158 .unwrap();
6159 assert_eq!(
6160 opt.validate_isolation_options().unwrap_err().to_string(),
6161 "--snp-restricted-injection requires --hypervisor mshv"
6162 );
6163
6164 let opt = Options::try_parse_from([
6165 "openvmm",
6166 "--hypervisor",
6167 "mshv",
6168 "--isolation",
6169 "snp",
6170 "--snp-restricted-injection",
6171 ])
6172 .unwrap();
6173 opt.validate_isolation_options().unwrap();
6174
6175 let opt = Options::try_parse_from([
6176 "openvmm",
6177 "--hypervisor",
6178 "mshv",
6179 "--isolation",
6180 "snp",
6181 "--snp-restricted-injection",
6182 "--pcat",
6183 ])
6184 .unwrap();
6185 assert_eq!(
6186 opt.validate_isolation_options().unwrap_err().to_string(),
6187 "--snp-restricted-injection requires Linux direct boot"
6188 );
6189
6190 let opt = Options::try_parse_from([
6191 "openvmm",
6192 "--hypervisor",
6193 "mshv",
6194 "--isolation",
6195 "snp",
6196 "--snp-restricted-injection",
6197 "--restore-snapshot",
6198 "snapshot",
6199 ])
6200 .unwrap();
6201 assert_eq!(
6202 opt.validate_isolation_options().unwrap_err().to_string(),
6203 "--snp-restricted-injection requires Linux direct boot"
6204 );
6205 }
6206
6207 #[test]
6208 fn test_isolation_options_allow_vbs_uefi() {
6209 let opt = Options::try_parse_from(["openvmm", "--isolation", "vbs", "--uefi"]).unwrap();
6210
6211 opt.validate_isolation_options().unwrap();
6212 }
6213
6214 #[test]
6215 fn test_isolation_options_allow_vbs_hugepages() {
6216 let opt = Options::try_parse_from([
6217 "openvmm",
6218 "--isolation",
6219 "vbs",
6220 "--memory",
6221 "size=1G,hugepages=on",
6222 ])
6223 .unwrap();
6224
6225 opt.validate_isolation_options().unwrap();
6226 }
6227
6228 #[test]
6229 fn test_igvm_requires_firmware_and_personality() {
6230 for (options, missing) in [
6231 ("firmware=guest.igvm", "personality"),
6232 ("personality=openhcl", "firmware"),
6233 ] {
6234 for extra in [vec![], vec!["--hv", "--vtl2"]] {
6235 let mut args = vec!["openvmm", "--igvm", options];
6236 args.extend(extra);
6237 let err = Options::try_parse_from(args).err().unwrap();
6238 assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
6239 assert!(err.to_string().contains(missing));
6240 assert!(err.to_string().contains("required"));
6241 }
6242 }
6243 let opt = Options::try_parse_from(["openvmm"]).unwrap();
6244 opt.validate_igvm_options().unwrap();
6245 }
6246
6247 #[test]
6248 fn test_igvm_personality_values() {
6249 for (value, expected) in [
6250 ("openhcl", IgvmPersonalityCli::Openhcl),
6251 ("uefi", IgvmPersonalityCli::Uefi),
6252 ("linux-direct", IgvmPersonalityCli::LinuxDirect),
6253 ] {
6254 let options = format!("firmware=guest.igvm,personality={value}");
6255 let mut args = vec!["openvmm", "--igvm", &options];
6256 if expected == IgvmPersonalityCli::Openhcl {
6257 args.extend(["--hv", "--vtl2"]);
6258 }
6259 let opt = Options::try_parse_from(args).unwrap();
6260 opt.validate_igvm_options().unwrap();
6261 assert_eq!(
6262 opt.igvm,
6263 Some(IgvmCli {
6264 firmware: PathBuf::from("guest.igvm"),
6265 personality: expected,
6266 })
6267 );
6268 }
6269 }
6270
6271 #[test]
6272 fn test_igvm_options_accept_reordered_keys_and_windows_paths() {
6273 let opt = Options::try_parse_from([
6274 "openvmm",
6275 "--igvm",
6276 r"personality=uefi,firmware=C:\VM Images\guest=1.igvm",
6277 ])
6278 .unwrap();
6279 assert_eq!(
6280 opt.igvm,
6281 Some(IgvmCli {
6282 firmware: PathBuf::from(r"C:\VM Images\guest=1.igvm"),
6283 personality: IgvmPersonalityCli::Uefi,
6284 })
6285 );
6286 }
6287
6288 #[test]
6289 fn test_igvm_options_reject_invalid_values() {
6290 for options in [
6291 "",
6292 "guest.igvm",
6293 "firmware=,personality=uefi",
6294 "firmware=guest.igvm,personality=",
6295 "firmware=guest.igvm,personality=pcat",
6296 "firmware=guest.igvm,personality=UEFI",
6297 "firmware=guest.igvm,personality=uefi,unknown=on",
6298 "firmware=guest.igvm,firmware=other.igvm,personality=uefi",
6299 "firmware=guest.igvm,personality=uefi,personality=linux-direct",
6300 ] {
6301 let err = Options::try_parse_from(["openvmm", "--igvm", options])
6302 .err()
6303 .unwrap();
6304 assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
6305 }
6306 }
6307
6308 #[test]
6309 fn test_igvm_rejects_separate_personality_flag() {
6310 let err = Options::try_parse_from([
6311 "openvmm",
6312 "--igvm",
6313 "firmware=guest.igvm,personality=uefi",
6314 "--igvm-personality",
6315 "uefi",
6316 ])
6317 .err()
6318 .unwrap();
6319 assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
6320 }
6321
6322 #[test]
6323 fn test_openhcl_igvm_personality() {
6324 let opt = Options::try_parse_from([
6325 "openvmm",
6326 "--igvm",
6327 "firmware=guest.igvm,personality=openhcl",
6328 "--hv",
6329 "--vtl2",
6330 ])
6331 .unwrap();
6332 opt.validate_igvm_options().unwrap();
6333 assert_eq!(
6334 opt.igvm.as_ref().unwrap().personality,
6335 IgvmPersonalityCli::Openhcl
6336 );
6337 assert!(opt.effective_uefi().unwrap().is_none());
6338 }
6339
6340 #[test]
6341 fn test_openhcl_igvm_requires_explicit_vtl2_and_hv() {
6342 for extra in [vec![], vec!["--hv"], vec!["--vtl2"]] {
6343 let mut args = vec![
6344 "openvmm",
6345 "--igvm",
6346 "firmware=guest.igvm,personality=openhcl",
6347 ];
6348 args.extend(extra);
6349 match Options::try_parse_from(args) {
6350 Ok(opt) => assert_eq!(
6351 opt.validate_igvm_options().unwrap_err().to_string(),
6352 "--igvm personality=openhcl requires --hv --vtl2"
6353 ),
6354 Err(err) => {
6355 assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
6356 assert!(err.to_string().contains("--hv"));
6357 }
6358 }
6359 }
6360 }
6361
6362 #[test]
6363 fn test_non_openhcl_igvm_personalities_reject_vtl2_and_uefi() {
6364 for personality in ["uefi", "linux-direct"] {
6365 let options = format!("firmware=guest.igvm,personality={personality}");
6366 for extra in [vec!["--uefi"], vec!["--hv", "--vtl2"]] {
6367 let mut args = vec!["openvmm", "--igvm", &options];
6368 args.extend(extra);
6369 let opt = Options::try_parse_from(args).unwrap();
6370 assert!(opt.validate_igvm_options().is_err());
6371 }
6372 }
6373 }
6374
6375 #[test]
6376 fn test_openhcl_igvm_accepts_uefi_settings() {
6377 let opt = Options::try_parse_from([
6378 "openvmm",
6379 "--igvm",
6380 "firmware=guest.igvm,personality=openhcl",
6381 "--hv",
6382 "--vtl2",
6383 "--uefi",
6384 "console=com1,disable_frontpage",
6385 ])
6386 .unwrap();
6387 opt.validate_igvm_options().unwrap();
6388 let uefi = opt.effective_uefi().unwrap().unwrap();
6389 assert!(matches!(uefi.console, Some(UefiConsoleModeCli::Com1)));
6390 assert!(uefi.disable_frontpage);
6391 }
6392
6393 #[test]
6394 fn test_igvm_personality_conflicts_with_pcat() {
6395 for personality in ["openhcl", "uefi", "linux-direct"] {
6396 let options = format!("firmware=guest.igvm,personality={personality}");
6397 assert!(Options::try_parse_from(["openvmm", "--igvm", &options, "--pcat"]).is_err());
6398 }
6399 }
6400
6401 #[test]
6402 fn test_pidfile_option_parsed() {
6403 let opt = Options::try_parse_from(["openvmm", "--pidfile", "/tmp/test.pid"]).unwrap();
6404 assert_eq!(opt.pidfile, Some(PathBuf::from("/tmp/test.pid")));
6405 }
6406
6407 #[test]
6408 fn test_tpm_version_option() {
6409 let opt = Options::try_parse_from(["openvmm"]).unwrap();
6410 assert_eq!(opt.tpm, None);
6411
6412 let opt = Options::try_parse_from(["openvmm", "--tpm"]).unwrap();
6413 assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
6414
6415 let opt = Options::try_parse_from(["openvmm", "--tpm", "--uefi"]).unwrap();
6416 assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
6417 assert!(opt.uefi.is_some());
6418
6419 let opt = Options::try_parse_from(["openvmm", "--tpm", "138"]).unwrap();
6420 assert_eq!(opt.tpm, Some(TpmVersionCli::V138));
6421
6422 let opt = Options::try_parse_from(["openvmm", "--tpm=185"]).unwrap();
6423 assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
6424
6425 let opt = Options::try_parse_from(["openvmm", "--tpm", "1.38"]).unwrap();
6426 assert_eq!(opt.tpm, Some(TpmVersionCli::V138));
6427
6428 assert!(Options::try_parse_from(["openvmm", "--tpm", "137"]).is_err());
6429 }
6430
6431 #[test]
6432 fn test_uefi_options() {
6433 let options = Options::try_parse_from(["openvmm", "--uefi"]).unwrap();
6434 assert!(options.effective_uefi().unwrap().is_some());
6435
6436 let options = Options::try_parse_from([
6437 "openvmm",
6438 "--uefi",
6439 "firmware=foo/bar,debug,force_firmware_version,console=com1,diagnostics=full",
6440 ])
6441 .unwrap();
6442 let uefi = options.effective_uefi().unwrap().unwrap();
6443 assert_eq!(uefi.firmware, Some("foo/bar".into()));
6444 assert!(uefi.debug);
6445 assert!(uefi.force_firmware_version);
6446 assert!(matches!(uefi.console, Some(UefiConsoleModeCli::Com1)));
6447 assert!(matches!(
6448 uefi.diagnostics,
6449 Some(EfiDiagnosticsLogLevelCli::Full)
6450 ));
6451
6452 let options = Options::try_parse_from([
6453 "openvmm",
6454 "--uefi",
6455 "--uefi-firmware",
6456 "legacy.fd",
6457 "--uefi-debug",
6458 ])
6459 .unwrap();
6460 let uefi = options.effective_uefi().unwrap().unwrap();
6461 assert_eq!(uefi.firmware, Some("legacy.fd".into()));
6462 assert!(uefi.debug);
6463
6464 assert!(Options::try_parse_from(["openvmm", "--uefi", "unknown"]).is_err());
6465 let options = Options::try_parse_from(["openvmm", "--uefi", "debug=on"]).unwrap();
6466 assert!(options.effective_uefi().unwrap().unwrap().debug);
6467 let options = Options::try_parse_from(["openvmm", "--uefi", "debug=off"]).unwrap();
6468 assert!(!options.effective_uefi().unwrap().unwrap().debug);
6469 assert!(Options::try_parse_from(["openvmm", "--uefi", "debug,debug"]).is_err());
6470
6471 let options = Options::try_parse_from([
6472 "openvmm",
6473 "--uefi",
6474 "firmware=new.fd",
6475 "--uefi-firmware",
6476 "legacy.fd",
6477 ])
6478 .unwrap();
6479 assert!(options.effective_uefi().is_err());
6480 }
6481
6482 #[test]
6483 fn test_guest_power_action_flags() {
6484 let opt = Options::try_parse_from(["openvmm"]).unwrap();
6487 assert_eq!(opt.guest_reset_action, GuestPowerAction::Reset);
6488 assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Halt);
6489 assert_eq!(opt.guest_crash_action, GuestPowerAction::Halt);
6490 assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Reset);
6491 assert_eq!(
6494 crate::vm_controller::GuestPowerActions {
6495 shutdown: opt.guest_shutdown_action,
6496 reset: opt.guest_reset_action,
6497 crash: opt.guest_crash_action,
6498 watchdog: opt.guest_watchdog_action,
6499 },
6500 crate::vm_controller::GuestPowerActions::default(),
6501 );
6502
6503 let opt = Options::try_parse_from([
6504 "openvmm",
6505 "--guest-watchdog",
6506 "--guest-reset-action",
6507 "exit",
6508 "--guest-shutdown-action",
6509 "exit:5",
6510 "--guest-crash-action",
6511 "reset",
6512 "--guest-watchdog-action",
6513 "halt",
6514 ])
6515 .unwrap();
6516 assert_eq!(opt.guest_reset_action, GuestPowerAction::Exit(0));
6518 assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Exit(5));
6519 assert_eq!(opt.guest_crash_action, GuestPowerAction::Reset);
6520 assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Halt);
6521
6522 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:nope"]).is_err());
6524 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:300"]).is_err());
6525 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:-1"]).is_err());
6526
6527 assert!(Options::try_parse_from(["openvmm", "--guest-watchdog-action", "halt"]).is_err());
6529 }
6530
6531 #[cfg(target_os = "linux")]
6532 #[test]
6533 fn test_vfio_device_cli_parse() {
6534 use vfio_assigned_device_resources::BarAddressConfig;
6535
6536 let v = VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0").unwrap();
6538 assert_eq!(v.pci_id, "0000:01:00.0");
6539 assert_eq!(v.port_name, "rp0");
6540 assert_eq!(v.iommu, None);
6541
6542 let v = VfioDeviceCli::from_str("port=rp1,iommu=iommu0,host=0000:02:00.0").unwrap();
6544 assert_eq!(v.pci_id, "0000:02:00.0");
6545 assert_eq!(v.port_name, "rp1");
6546 assert_eq!(v.iommu.as_deref(), Some("iommu0"));
6547
6548 let v = VfioDeviceCli::from_str(
6549 "host=0000:03:00.0,port=rp2,bar0=host,bar2=0x80000000,bar4=0x110000000000",
6550 )
6551 .unwrap();
6552 assert_eq!(v.bar_addresses[0], BarAddressConfig::HostAssigned);
6553 assert_eq!(v.bar_addresses[1], BarAddressConfig::GuestAssigned);
6554 assert_eq!(v.bar_addresses[2], BarAddressConfig::Fixed(0x80000000));
6555 assert_eq!(v.bar_addresses[4], BarAddressConfig::Fixed(0x110000000000));
6556 }
6557
6558 #[cfg(target_os = "linux")]
6559 #[test]
6560 fn test_vfio_device_cli_errors() {
6561 assert!(VfioDeviceCli::from_str("port=rp0").is_err());
6563 assert!(VfioDeviceCli::from_str("host=0000:01:00.0").is_err());
6564
6565 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,foo=bar").is_err());
6567
6568 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,host=0000:02:00.0,port=rp0").is_err());
6570 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,port=rp1").is_err());
6571 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=a,iommu=b").is_err());
6572
6573 assert!(VfioDeviceCli::from_str("host=,port=rp0").is_err());
6575 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=").is_err());
6576 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=").is_err());
6577
6578 assert!(VfioDeviceCli::from_str("host").is_err());
6580 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu").is_err());
6581
6582 assert!(VfioDeviceCli::from_str("host=../../etc/passwd,port=rp0").is_err());
6584 assert!(VfioDeviceCli::from_str("host=foo/bar,port=rp0").is_err());
6585
6586 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0").is_err());
6588 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x0").is_err());
6589 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0xnope").is_err());
6590 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=pt").is_err());
6591 assert!(
6592 VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x1000,bar0=host").is_err()
6593 );
6594 }
6595
6596 #[cfg(target_os = "linux")]
6597 #[test]
6598 fn test_iommu_cli_parse() {
6599 let c = IommuCli::from_str("id=iommu0").unwrap();
6600 assert_eq!(c.id, "iommu0");
6601
6602 assert!(IommuCli::from_str("name=iommu0").is_err());
6604
6605 assert!(IommuCli::from_str("iommu0").is_err());
6607
6608 assert!(IommuCli::from_str("id=").is_err());
6610 }
6611
6612 #[cfg(target_os = "linux")]
6613 #[test]
6614 fn test_vhost_user_cli() {
6615 let v = VhostUserCli::from_str("/run/blk.sock,type=blk,num_queues=2").unwrap();
6617 assert_eq!(v.socket_path, "/run/blk.sock");
6618 assert!(matches!(
6619 v.device_type,
6620 VhostUserDeviceTypeCli::Blk {
6621 num_queues: Some(2),
6622 queue_size: None
6623 }
6624 ));
6625 assert_eq!(v.pcie_port, None);
6626
6627 let v = VhostUserCli::from_str("/run/fs.sock,type=fs,tag=myfs,pcie_port=p0").unwrap();
6629 assert!(matches!(
6630 &v.device_type,
6631 VhostUserDeviceTypeCli::Fs { tag, .. } if tag == "myfs"
6632 ));
6633 assert_eq!(v.pcie_port.as_deref(), Some("p0"));
6634
6635 let v = VhostUserCli::from_str("/run/x.sock,device_id=9,queue_sizes=[16,32]").unwrap();
6637 assert!(matches!(
6638 &v.device_type,
6639 VhostUserDeviceTypeCli::Other { device_id: 9, queue_sizes } if *queue_sizes == vec![16, 32]
6640 ));
6641
6642 assert!(VhostUserCli::from_str("/run/x.sock").is_err()); assert!(VhostUserCli::from_str("/run/x.sock,type=blk,device_id=1").is_err()); assert!(VhostUserCli::from_str("/run/x.sock,type=fs").is_err()); assert!(VhostUserCli::from_str("/run/x.sock,type=zzz").is_err()); assert!(VhostUserCli::from_str("/run/x.sock,type=blk,tag=t").is_err()); assert!(VhostUserCli::from_str("/run/x.sock,type=blk,queue_sizes=[1]").is_err()); assert!(VhostUserCli::from_str("/run/x.sock,device_id=1,num_queues=2").is_err()); assert!(VhostUserCli::from_str("/run/x.sock,device_id=1,queue_sizes=[]").is_err()); assert!(VhostUserCli::from_str("/run/x.sock,device_id=1").is_err()); }
6653
6654 #[cfg(target_os = "linux")]
6655 #[test]
6656 fn test_vhost_vsock_cli() {
6657 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "3"]).unwrap();
6658 assert_eq!(opt.virtio_vsock_vhost_cid, Some(3));
6659
6660 let opt =
6661 Options::try_parse_from(["openvmm", "--virtio-vsock-path", "/tmp/vsock"]).unwrap();
6662 assert_eq!(opt.virtio_vsock_path.as_deref(), Some("/tmp/vsock"));
6663
6664 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "2"]).is_err());
6665 assert!(
6666 Options::try_parse_from([
6667 "openvmm",
6668 "--virtio-vsock-vhost-cid",
6669 "3",
6670 "--virtio-vsock-path",
6671 "/tmp/vsock",
6672 ])
6673 .is_err()
6674 );
6675 }
6676
6677 #[test]
6678 fn test_virtio_vsock_bus_cli() {
6679 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "mmio"]).unwrap();
6680 assert_eq!(opt.virtio_vsock_bus, Some(VirtioBusCli::Mmio));
6681
6682 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pci"]).unwrap();
6683 assert_eq!(opt.virtio_vsock_bus, Some(VirtioBusCli::Pci));
6684
6685 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie"]).unwrap();
6686 assert_eq!(
6687 opt.virtio_vsock_bus,
6688 Some(VirtioBusCli::Pcie("vsock".to_string()))
6689 );
6690
6691 let opt =
6692 Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie:custom"]).unwrap();
6693 assert_eq!(
6694 opt.virtio_vsock_bus,
6695 Some(VirtioBusCli::Pcie("custom".to_string()))
6696 );
6697
6698 let opt =
6699 Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "PCIE:custom"]).unwrap();
6700 assert_eq!(
6701 opt.virtio_vsock_bus,
6702 Some(VirtioBusCli::Pcie("custom".to_string()))
6703 );
6704
6705 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "auto"]).is_err());
6706 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "vpci"]).is_err());
6707 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie:"]).is_err());
6708 assert!(
6709 Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie:custom:extra"])
6710 .is_err()
6711 );
6712
6713 let opt =
6714 Options::try_parse_from(["openvmm", "--virtio-rng", "--virtio-rng-bus", "pcie:custom"])
6715 .unwrap();
6716 assert_eq!(opt.virtio_rng_bus, VirtioBusCli::Pcie("custom".to_string()));
6717 }
6718
6719 #[test]
6720 fn test_virtio_rtc_cli() {
6721 let opt = Options::try_parse_from(["openvmm"]).unwrap();
6722 assert!(opt.virtio_rtc.is_none());
6723
6724 let opt =
6725 Options::try_parse_from(["openvmm", "--virtio-rtc", "--processors", "2"]).unwrap();
6726 assert_eq!(
6727 opt.virtio_rtc,
6728 Some(VirtioRtcCli {
6729 bus: VirtioBusCli::Auto
6730 })
6731 );
6732
6733 let opt = Options::try_parse_from(["openvmm", "--virtio-rtc", "bus=pcie:rp0"]).unwrap();
6734 assert_eq!(
6735 opt.virtio_rtc,
6736 Some(VirtioRtcCli {
6737 bus: VirtioBusCli::Pcie("rp0".to_string())
6738 })
6739 );
6740
6741 for value in ["mmio", "bus", "bus=", "bus=pcie:", "bus=pcie:rp0,bus=mmio"] {
6742 assert!(Options::try_parse_from(["openvmm", "--virtio-rtc", value]).is_err());
6743 }
6744 }
6745
6746 #[test]
6747 fn test_nvme_controller_cli_pcie() {
6748 let c = NvmeControllerCli::from_str("id=nvme0,pcie_port=p0").unwrap();
6749 assert_eq!(c.id, "nvme0");
6750 assert_eq!(c.transport, NvmeControllerTransport::Pcie("p0".into()));
6751 }
6752
6753 #[test]
6754 fn test_nvme_controller_cli_vpci_no_guid() {
6755 let c = NvmeControllerCli::from_str("id=nvme1,vpci").unwrap();
6756 assert_eq!(c.id, "nvme1");
6757 assert!(matches!(c.transport, NvmeControllerTransport::Vpci(None)));
6758 assert!(NvmeControllerCli::from_str("id=nvme1,vpci=").is_err());
6759 }
6760
6761 #[test]
6762 fn test_nvme_controller_cli_vpci_with_guid() {
6763 let c = NvmeControllerCli::from_str("id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c")
6764 .unwrap();
6765 assert_eq!(c.id, "nvme2");
6766 assert!(matches!(
6767 c.transport,
6768 NvmeControllerTransport::Vpci(Some(_))
6769 ));
6770 }
6771
6772 #[test]
6773 fn test_nvme_controller_cli_errors() {
6774 assert!(NvmeControllerCli::from_str("pcie_port=p0").is_err());
6776 assert!(NvmeControllerCli::from_str("id=nvme0").is_err());
6778 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,vpci").is_err());
6780 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,foo=bar").is_err());
6782 assert!(NvmeControllerCli::from_str("id=,pcie_port=p0").is_err());
6784 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=").is_err());
6786 let err = NvmeControllerCli::from_str("id=nvme0,vpci=not-a-guid").unwrap_err();
6788 assert!(err.to_string().contains("invalid value for option 'vpci'"));
6789 }
6790
6791 #[test]
6792 fn test_disk_cli_controller() {
6793 let d = DiskCli::from_str("file:disk.vhd,on=nvme0").unwrap();
6794 assert_eq!(d.controller.as_deref(), Some("nvme0"));
6795 assert_eq!(d.nsid, None);
6796 }
6797
6798 #[test]
6799 fn test_disk_cli_serial() {
6800 let d = DiskCli::from_str("file:disk.vhd,serial=DATA-DISK").unwrap();
6801 assert_eq!(d.serial.as_deref(), Some("DATA-DISK"));
6802 assert!(DiskCli::from_str("file:disk.vhd,serial=").is_err());
6803 assert!(DiskCli::from_str("file:disk.vhd,serial=DATA[1]").is_err());
6804 assert!(DiskCli::from_str("file:disk.vhd,serial=DATA,DISK").is_err());
6805 assert!(DiskCli::from_str("file:disk.vhd,serial=DATA\u{1f}DISK").is_err());
6806 assert!(DiskCli::from_str("file:disk.vhd,serial=non-ascii-\u{e9}").is_err());
6807 }
6808
6809 #[test]
6810 fn test_disk_cli_controller_with_nsid() {
6811 let d = DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=3").unwrap();
6812 assert_eq!(d.controller.as_deref(), Some("nvme0"));
6813 assert_eq!(d.nsid, Some(3));
6814 }
6815
6816 #[test]
6817 fn test_disk_cli_controller_errors() {
6818 assert!(DiskCli::from_str("file:disk.vhd,nsid=1").is_err());
6820 assert!(DiskCli::from_str("file:disk.vhd,lun=0").is_err());
6822 assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,pcie_port=p0").is_err());
6824 assert!(DiskCli::from_str("file:disk.vhd,on=").is_err());
6826 assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=abc").is_err());
6828 assert!(DiskCli::from_str("file:disk.vhd,on=c,nsid=1,lun=0").is_err());
6830 }
6831
6832 #[test]
6833 fn test_disk_cli_controller_with_lun() {
6834 let d = DiskCli::from_str("file:disk.vhd,on=scsi0,lun=3").unwrap();
6835 assert_eq!(d.controller.as_deref(), Some("scsi0"));
6836 assert_eq!(d.lun, Some(3));
6837 assert_eq!(d.nsid, None);
6838 }
6839
6840 #[test]
6841 fn test_scsi_controller_cli() {
6842 let c = ScsiControllerCli::from_str("id=scsi0").unwrap();
6843 assert_eq!(c.id, "scsi0");
6844 assert_eq!(c.sub_channels, 0);
6845 }
6846
6847 #[test]
6848 fn test_scsi_controller_cli_with_sub_channels() {
6849 let c = ScsiControllerCli::from_str("id=scsi1,sub_channels=4").unwrap();
6850 assert_eq!(c.id, "scsi1");
6851 assert_eq!(c.sub_channels, 4);
6852 }
6853
6854 #[test]
6855 fn test_scsi_controller_cli_errors() {
6856 assert!(ScsiControllerCli::from_str("sub_channels=4").is_err());
6858 assert!(ScsiControllerCli::from_str("id=").is_err());
6860 assert!(ScsiControllerCli::from_str("id=scsi0,foo=bar").is_err());
6862 assert!(ScsiControllerCli::from_str("id=scsi0,sub_channels=abc").is_err());
6864 }
6865
6866 #[test]
6867 fn test_disk_cli_relay() {
6868 let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt").unwrap();
6869 assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
6870 assert_eq!(d.relay.as_ref().unwrap().1, None);
6871 }
6872
6873 #[test]
6874 fn test_disk_cli_relay_with_location() {
6875 let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:3").unwrap();
6876 assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
6877 assert_eq!(d.relay.as_ref().unwrap().1, Some(3));
6878 }
6879
6880 #[test]
6881 fn test_disk_cli_relay_errors() {
6882 assert!(DiskCli::from_str("file:disk.vhd,relay=tgt").is_err());
6884 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt,uh").is_err());
6886 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:abc").is_err());
6888 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=").is_err());
6890 }
6891
6892 #[test]
6893 fn test_nvme_controller_cli_vtl2() {
6894 let c = NvmeControllerCli::from_str("id=nvme0,vpci,vtl2").unwrap();
6895 assert_eq!(c.vtl, DeviceVtl::Vtl2);
6896 }
6897
6898 #[test]
6899 fn test_scsi_controller_cli_vtl2() {
6900 let c = ScsiControllerCli::from_str("id=scsi0,vtl2").unwrap();
6901 assert_eq!(c.vtl, DeviceVtl::Vtl2);
6902 }
6903
6904 #[test]
6905 fn test_openhcl_controller_cli() {
6906 let c = OpenhclControllerCli::from_str("id=vtl0-scsi,type=scsi").unwrap();
6907 assert_eq!(c.id, "vtl0-scsi");
6908 assert_eq!(c.controller_type, OpenhclControllerType::Scsi);
6909 assert_eq!(c.guid, None);
6910 }
6911
6912 #[test]
6913 fn test_openhcl_controller_cli_nvme_with_guid() {
6914 let c = OpenhclControllerCli::from_str(
6915 "id=vtl0-nvme,type=nvme,guid=09a59b81-2bf6-4164-81d7-3a0dc977ba65",
6916 )
6917 .unwrap();
6918 assert_eq!(c.controller_type, OpenhclControllerType::Nvme);
6919 assert!(c.guid.is_some());
6920 }
6921
6922 #[test]
6923 fn test_openhcl_controller_cli_errors() {
6924 assert!(OpenhclControllerCli::from_str("type=scsi").is_err());
6926 assert!(OpenhclControllerCli::from_str("id=foo").is_err());
6928 assert!(OpenhclControllerCli::from_str("id=foo,type=ide").is_err());
6930 assert!(OpenhclControllerCli::from_str("id=foo,type=scsi,guid=bad").is_err());
6932 }
6933
6934 #[test]
6935 fn test_parse_vp_list() {
6936 use vmm_cli::BracketRangeList;
6937
6938 let parse = |s: &str| {
6939 s.parse::<BracketRangeList>()
6940 .and_then(|v| v.expand_below(1024))
6941 };
6942
6943 assert_eq!(parse("[0,1,2,3]").unwrap(), vec![0, 1, 2, 3]);
6945
6946 assert_eq!(parse("[5]").unwrap(), vec![5]);
6948
6949 assert_eq!(parse("[0-3]").unwrap(), vec![0, 1, 2, 3]);
6951
6952 assert_eq!(parse("[0,1,4-6,10]").unwrap(), vec![0, 1, 4, 5, 6, 10]);
6954
6955 assert_eq!(parse("[0, 1, 2-4]").unwrap(), vec![0, 1, 2, 3, 4]);
6957
6958 assert!(parse("0,1,2").is_err());
6960 assert!(parse("0-3").is_err());
6961
6962 assert!(parse("[3-0]").is_err());
6964
6965 assert!(parse("[a,b]").is_err());
6967 }
6968
6969 #[test]
6970 fn test_parse_numa_node() {
6971 use super::parse_numa_node;
6972
6973 let n = parse_numa_node("size=2G").unwrap();
6975 assert_eq!(
6976 n.memory.size,
6977 Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024))
6978 );
6979 assert!(n.vps.is_none());
6980 assert!(n.host_numa_node.is_none());
6981
6982 let n = parse_numa_node("size=1G,vps=[0,1,2,3]").unwrap();
6984 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
6985
6986 let n = parse_numa_node("size=1G,vps=[0-3]").unwrap();
6988 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
6989
6990 let n = parse_numa_node("size=1G,host_numa_node=1").unwrap();
6992 assert_eq!(n.host_numa_node, Some(1));
6993
6994 let n = parse_numa_node("size=1G,vps=[0,1],host_numa_node=0,hugepages=on").unwrap();
6996 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1]);
6997 assert_eq!(n.host_numa_node, Some(0));
6998 assert!(n.memory.hugepages);
6999
7000 assert!(parse_numa_node("vps=[0,1]").is_err());
7002
7003 assert!(parse_numa_node("size=1G,vps=0,1").is_err());
7005
7006 assert!(parse_numa_node("size=1G,vps=[0],vps=[1]").is_err());
7008
7009 assert!(parse_numa_node("size=1G,file=/tmp/x").is_err());
7011
7012 let n = parse_numa_node("size=1G,vps=[]").unwrap();
7014 assert!(n.vps.unwrap().0.is_empty());
7015 }
7016
7017 #[test]
7018 fn test_parse_numa_distance() {
7019 use super::parse_numa_distance;
7020
7021 let d = parse_numa_distance("0:1:20").unwrap();
7022 assert_eq!(d.src, 0);
7023 assert_eq!(d.dst, 1);
7024 assert_eq!(d.distance, 20);
7025
7026 let d = parse_numa_distance("0:0:10").unwrap();
7028 assert_eq!(d.distance, 10);
7029
7030 assert!(parse_numa_distance("0:1:5").is_err());
7032
7033 assert!(parse_numa_distance("0:1").is_err());
7035 assert!(parse_numa_distance("0:1:20:extra").is_err());
7036 }
7037
7038 #[cfg(guest_arch = "aarch64")]
7039 #[test]
7040 fn test_smmu_cli_from_str() {
7041 let s = SmmuCli::from_str("rc=pcie0").unwrap();
7043 assert_eq!(s.rc_name, "pcie0");
7044 assert!(!s.accel);
7045 assert!(matches!(s.oas, SmmuOasCli::Auto));
7046 assert!(matches!(s.ssidsize, SmmuSsidSizeCli::Auto));
7047
7048 let s = SmmuCli::from_str("rc=pcie0,accel").unwrap();
7050 assert_eq!(s.rc_name, "pcie0");
7051 assert!(s.accel);
7052 assert!(matches!(s.oas, SmmuOasCli::Auto));
7053 assert!(matches!(s.ssidsize, SmmuSsidSizeCli::Auto));
7054
7055 let s = SmmuCli::from_str("rc=pcie0,oas=auto").unwrap();
7057 assert!(matches!(s.oas, SmmuOasCli::Auto));
7058
7059 let s = SmmuCli::from_str("rc=pcie0,oas=52").unwrap();
7061 assert!(matches!(s.oas, SmmuOasCli::Fixed(52)));
7062
7063 let s = SmmuCli::from_str("ssidsize=12,oas=48,accel,rc=pcie1").unwrap();
7065 assert_eq!(s.rc_name, "pcie1");
7066 assert!(s.accel);
7067 assert!(matches!(s.oas, SmmuOasCli::Fixed(48)));
7068 assert!(matches!(s.ssidsize, SmmuSsidSizeCli::Fixed(12)));
7069
7070 assert!(SmmuCli::from_str("accel").is_err());
7072 assert!(SmmuCli::from_str("oas=52").is_err());
7073
7074 assert!(SmmuCli::from_str("rc=").is_err());
7076
7077 assert!(SmmuCli::from_str("rc=pcie0,rc=pcie1").is_err());
7079
7080 assert!(SmmuCli::from_str("rc=pcie0,oas=big").is_err());
7082
7083 assert!(SmmuCli::from_str("rc=pcie0,foo=bar").is_err());
7085
7086 assert!(SmmuCli::from_str("rc=pcie0,turbo").is_err());
7088 }
7089
7090 #[cfg(guest_arch = "aarch64")]
7091 #[test]
7092 fn test_smmu_cli_ssidsize() {
7093 for accel in ["", ",accel"] {
7094 let s = SmmuCli::from_str(&format!("rc=pcie0,ssidsize=auto{accel}")).unwrap();
7095 assert!(matches!(s.ssidsize, SmmuSsidSizeCli::Auto));
7096
7097 for bits in [0, 14, 20, 21, 255] {
7099 let s = SmmuCli::from_str(&format!("rc=pcie0,ssidsize={bits}{accel}")).unwrap();
7100 assert!(matches!(s.ssidsize, SmmuSsidSizeCli::Fixed(n) if n == bits));
7101 }
7102 }
7103
7104 for value in ["256", "-1", "1.5", "", "big", "AUTO"] {
7105 assert!(
7106 SmmuCli::from_str(&format!("rc=pcie0,ssidsize={value}")).is_err(),
7107 "accepted invalid ssidsize={value}"
7108 );
7109 }
7110 assert!(SmmuCli::from_str("ssidsize=12").is_err());
7111 assert!(SmmuCli::from_str("rc=pcie0,ssidsize=auto,ssidsize=12").is_err());
7112 }
7113}