1#![warn(missing_docs)]
20
21use anyhow::Context;
22use clap::Parser;
23use clap::ValueEnum;
24use cxl_spec::CfmwsWindowRestrictions;
25use guid::Guid;
26use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
27use openvmm_defs::config::DeviceVtl;
28use openvmm_defs::config::PcatBootDevice;
29use openvmm_defs::config::Vtl2BaseAddressType;
30use openvmm_defs::config::X2ApicConfig;
31use std::ffi::OsString;
32use std::net::SocketAddr;
33use std::path::PathBuf;
34use std::str::FromStr;
35use thiserror::Error;
36
37pub(crate) fn parse_options() -> Options {
41 fn on_big_stack<R: Send>(f: impl Send + FnOnce() -> R) -> R {
51 if cfg!(windows) {
52 std::thread::scope(|s| {
53 std::thread::Builder::new()
54 .stack_size(0x400000)
55 .spawn_scoped(s, f)
56 .unwrap()
57 .join()
58 .unwrap()
59 })
60 } else {
61 f()
62 }
63 }
64
65 on_big_stack(Options::parse)
66}
67
68const DEFAULT_MEMORY_SIZE: u64 = 1024 * 1024 * 1024;
69
70#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
77pub struct MemoryCli {
78 pub size: Option<vmm_cli::MemorySize>,
80 #[kv(flag)]
83 pub shared: Option<bool>,
84 #[kv(flag)]
86 pub prefetch: bool,
87 #[kv(flag, key = "thp")]
90 pub transparent_hugepages: Option<bool>,
91 #[kv(flag)]
93 pub hugepages: bool,
94 pub hugepage_size: Option<vmm_cli::MemorySize>,
96 pub file: Option<PathBuf>,
98}
99
100impl MemoryCli {
101 pub fn validate(&self) -> anyhow::Result<()> {
103 if self.hugepage_size.is_some() && !self.hugepages {
104 anyhow::bail!("hugepage_size requires hugepages=on");
105 }
106 if self.hugepages {
107 if self.shared == Some(false) {
108 anyhow::bail!("hugepages=on conflicts with shared=off");
109 }
110 if self.file.is_some() {
111 anyhow::bail!("hugepages=on conflicts with file=...");
112 }
113 }
114 Ok(())
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, vmm_cli::KeyValueArgs)]
120pub struct NumaNodeCli {
121 #[kv(flatten)]
123 pub memory: MemoryCli,
124 pub host_numa_node: Option<u32>,
126 pub vps: Option<vmm_cli::BracketRangeList>,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct NumaDistanceCli {
133 pub src: u32,
135 pub dst: u32,
137 pub distance: u8,
139}
140
141#[derive(Parser)]
146#[command(
147 name = "openvmm",
148 version = openvmm_build_info::get().version(),
149 long_version = openvmm_build_info::get().long_version(),
150)]
151pub struct Options {
152 #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
154 pub processors: u32,
155
156 #[clap(
158 short = 'm',
159 long,
160 value_name = "PARAMS",
161 default_value = "1GB",
162 value_parser = parse_memory_config,
163 conflicts_with = "numa",
164 long_help = r#"Configure guest RAM.
165
166Syntax: SIZE | key=value[,key=value...]
167
168Size suffixes accept K, M, G, and T, optionally followed by B.
169
170Options:
171 size=<SIZE> guest RAM size, default 1GB
172 shared[=on|off] use shared file-backed RAM, default on
173 prefetch[=on|off] pre-populate guest RAM mappings
174 thp[=on|off] mark guest RAM as THP-eligible (Linux), default on
175 hugepages[=on|off] allocate RAM from hugetlb/large pages (Linux, Windows)
176 hugepage_size=<SIZE> hugepage size, default 2MB; requires hugepages=on
177 file=<PATH> use an existing file as guest RAM backing
178
179Examples:
180 --memory 4G
181 --memory size=64GB,hugepages=on,hugepage_size=2MB
182 --memory size=4G,file=path/to/memory.bin
183 --memory size=4G,thp=off"#
184 )]
185 pub memory: MemoryCli,
186
187 #[clap(
192 long,
193 value_name = "PARAMS",
194 value_parser = parse_numa_node,
195 conflicts_with = "memory",
196 long_help = r#"Configure a guest NUMA node (repeatable, one per node).
197
198Syntax: key=value[,key=value...]
199
200Options:
201 size=<SIZE> RAM for this node (required)
202 shared[=on|off] use shared file-backed RAM, default on
203 prefetch[=on|off] pre-populate guest RAM mappings
204 thp[=on|off] mark node RAM as THP-eligible (Linux), default on
205 hugepages[=on|off] allocate RAM from hugetlb/large pages (Linux, Windows)
206 hugepage_size=<SIZE> hugepage size, default 2MB; requires hugepages=on
207 host_numa_node=<N> bind allocation to host NUMA node N
208 vps=<LIST> explicit VP indices (e.g. "[0,1,2,3]")
209
210 VP lists use bracket syntax with comma-separated indices and dash
211 ranges: vps=[0,1] or vps=[0-3] or vps=[0,1,4-5]. An empty list, vps=[],
212 declares a CPU-less node (e.g. a generic-initiator target); unlike a
213 non-empty list, it may be combined with nodes that omit vps.
214
215Examples:
216 --numa size=2G --numa size=2G
217 --numa size=2G,host_numa_node=0 --numa size=2G,host_numa_node=1
218 --numa size=2G,hugepages=on,vps=[0,1] --numa size=2G,vps=[2,3]
219 --numa size=2G,vps=[0-3] --numa size=2G,vps=[4-7]
220 --numa size=2G --numa size=0,vps=[]"#
221 )]
222 pub numa: Option<Vec<NumaNodeCli>>,
223
224 #[clap(long, value_name = "SRC:DST:DIST", value_parser = parse_numa_distance, conflicts_with = "memory", requires = "numa")]
229 pub numa_distance: Option<Vec<NumaDistanceCli>>,
230
231 #[clap(short = 'M', long, hide = true)]
233 pub shared_memory: bool,
234
235 #[clap(long = "prefetch", hide = true, conflicts_with = "numa")]
237 pub deprecated_prefetch: bool,
238
239 #[clap(
243 long = "memory-backing-file",
244 value_name = "FILE",
245 hide = true,
246 conflicts_with_all = ["deprecated_private_memory", "numa"]
247 )]
248 pub deprecated_memory_backing_file: Option<PathBuf>,
249
250 #[clap(
253 long,
254 value_name = "DIR",
255 conflicts_with_all = ["deprecated_memory_backing_file", "numa"]
256 )]
257 pub restore_snapshot: Option<PathBuf>,
258
259 #[clap(long = "private-memory", hide = true, conflicts_with_all = ["deprecated_memory_backing_file", "restore_snapshot", "numa"])]
261 pub deprecated_private_memory: bool,
262
263 #[clap(long = "thp", hide = true, conflicts_with = "numa")]
265 pub deprecated_thp: bool,
266
267 #[clap(short = 'P', long)]
269 pub paused: bool,
270
271 #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
273 pub kernel: OptionalPathBuf,
274
275 #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
277 pub initrd: OptionalPathBuf,
278
279 #[clap(short = 'c', long, value_name = "STRING")]
281 pub cmdline: Vec<String>,
282
283 #[clap(long)]
285 pub hv: bool,
286
287 #[clap(
290 long,
291 requires_all = ["uefi", "no_vmbus"],
292 conflicts_with_all = ["hv", "vtl2", "get", "pcat", "igvm"]
293 )]
294 pub no_hv: bool,
295
296 #[clap(long, conflicts_with_all = ["uefi", "pcat", "igvm", "smbios"])]
300 pub device_tree: bool,
301
302 #[clap(
304 long,
305 value_name = "PARAMS",
306 value_parser = parse_smbios,
307 long_help = r#"Override the guest's SMBIOS (DMI) identity.
308
309Syntax: type=N,key=value[,key=value...]
310
311`type` selects the SMBIOS structure and is required; there is no default.
312Unset keys use the loader's built-in default identity. Repeat --smbios to set
313fields across multiple structure types.
314
315OpenVMM Linux direct boot supports all listed fields. OpenHCL Linux direct
316boot and UEFI boot support Type 1 fields only. PCAT boot supports only Type 1
317`serial` and `uuid`. Unsupported fields are rejected with an error.
318
319Type 0 (BIOS Information):
320 vendor=<STRING> BIOS vendor
321 version=<STRING> BIOS version
322 date=<STRING> BIOS release date
323 release=<MAJOR.MINOR> System BIOS Major/Minor Release (e.g. 4.1)
324
325Type 1 (System Information):
326 manufacturer=<STRING> system manufacturer (sys_vendor)
327 product=<STRING> product name (product_name)
328 version=<STRING> product version (product_version)
329 serial=<STRING> serial number (product_serial)
330 uuid=<GUID|random> system UUID (product_uuid); `random` generates a
331 fresh per-VM GUID. Unset => all-zero GUID.
332 sku=<STRING> SKU number (product_sku)
333 family=<STRING> product family (product_family)
334
335Examples:
336 --smbios type=1,manufacturer=Contoso,product="Virtual Machine"
337 --smbios type=1,uuid=12345678-9abc-def0-1234-56789abcdef0
338 --smbios type=1,uuid=random
339 --smbios type=0,vendor=Contoso,version=1.0"#
340 )]
341 pub smbios: Vec<SmbiosCli>,
342
343 #[clap(long, requires("hv"))]
347 pub vtl2: bool,
348
349 #[clap(long, requires("hv"))]
352 pub get: bool,
353
354 #[clap(long, conflicts_with("get"))]
357 pub no_get: bool,
358
359 #[clap(
361 long,
362 conflicts_with_all = [
363 "vmbus_vsock_path",
364 "vmbus_vtl2_vsock_path",
365 "vmbus_redirect",
366 "vmbus_max_version",
367 "vmbus_com1_serial",
368 "vmbus_com2_serial",
369 "vtl2",
370 "get",
371 "pcat",
372 ],
373 )]
374 pub no_vmbus: bool,
375
376 #[clap(long, requires("vtl2"))]
378 pub no_alias_map: bool,
379
380 #[clap(long)]
382 pub isolation: Option<IsolationCli>,
383
384 #[clap(long)]
386 pub snp_restricted_injection: bool,
387
388 #[clap(long, value_name = "PATH", alias = "vsock-path")]
390 pub vmbus_vsock_path: Option<String>,
391
392 #[clap(long, value_name = "PATH", requires("vtl2"), alias = "vtl2-vsock-path")]
394 pub vmbus_vtl2_vsock_path: Option<String>,
395
396 #[clap(long, requires("vtl2"), default_value = "halt")]
398 pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
399
400 #[clap(long_help = r#"
402e.g: --disk memdiff:file:/path/to/disk.vhd
403
404syntax: <path> | kind:<arg>[,flag,opt=arg,...]
405
406valid disk kinds:
407 `mem:<len>` memory backed disk
408 <len>: length of ramdisk, e.g.: `1G`
409 `memdiff:<disk>` memory backed diff disk
410 <disk>: lower disk, e.g.: `file:base.img`
411 `file:<path>[;direct][;create=<len>]` file-backed disk
412 <path>: path to file
413 `;direct`: bypass the OS page cache
414 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
415 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
416 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
417 `blob:<type>:<url>` HTTP blob (read-only)
418 <type>: `flat` or `vhd1`
419 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
420 <cipher>: `xts-aes-256`
421 `prwrap:<disk>` persistent reservations wrapper
422
423flags:
424 `ro` open disk as read-only
425 `dvd` specifies that device is cd/dvd and it is read_only
426 `vtl2` assign this disk to VTL2
427 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
428 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
429
430options:
431 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
432 `on=<name>` attach to a named controller (NVMe or SCSI), incompatible with `pcie_port` and `vtl2`
433 `nsid=<N>` NVMe namespace ID (1-based), requires `on`; auto-assigned if omitted
434 `lun=<N>` SCSI LUN (0-based), requires `on`; auto-assigned if omitted
435 `relay=<ctrl>[:<loc>]` relay through OpenHCL to the named OpenHCL controller, with optional location (LUN or NSID)
436"#)]
437 #[clap(long, value_name = "FILE")]
438 pub disk: Vec<DiskCli>,
439
440 #[clap(long_help = r#"
444e.g: --nvme memdiff:file:/path/to/disk.vhd
445
446syntax: <path> | kind:<arg>[,flag,opt=arg,...]
447
448valid disk kinds:
449 `mem:<len>` memory backed disk
450 <len>: length of ramdisk, e.g.: `1G`
451 `memdiff:<disk>` memory backed diff disk
452 <disk>: lower disk, e.g.: `file:base.img`
453 `file:<path>[;direct][;create=<len>]` file-backed disk
454 <path>: path to file
455 `;direct`: bypass the OS page cache
456 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
457 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
458 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
459 `blob:<type>:<url>` HTTP blob (read-only)
460 <type>: `flat` or `vhd1`
461 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
462 <cipher>: `xts-aes-256`
463 `prwrap:<disk>` persistent reservations wrapper
464
465flags:
466 `ro` open disk as read-only
467 `vtl2` assign this disk to VTL2
468 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
469 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
470
471options:
472 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
473"#)]
474 #[clap(long)]
475 pub nvme: Vec<DiskCli>,
476
477 #[clap(long_help = r#"
479Create a named NVMe controller with an explicit transport.
480
481syntax: id=<name>,pcie_port=<port> | id=<name>,vpci[=<guid>]
482
483The controller name can be referenced by `--disk` with the `on=<name>`
484option to attach namespaces to this controller.
485
486options:
487 `id=<name>` controller name (required)
488 `pcie_port=<port>` present on PCIe under the specified port
489 `vpci[=<guid>]` present via VPCI; optional instance GUID
490 `vtl2` assign to VTL2 (default VTL0)
491
492Exactly one of `pcie_port` or `vpci` must be specified.
493
494Examples:
495 --nvme-pci id=nvme0,pcie_port=p0
496 --nvme-pci id=nvme1,vpci
497 --nvme-pci id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c
498"#)]
499 #[clap(long = "nvme-pci")]
500 pub nvme_pci: Vec<NvmeControllerCli>,
501
502 #[clap(long_help = r#"
504Create a named VMBus SCSI controller.
505
506syntax: id=<name>[,sub_channels=<N>][,vtl2]
507
508The controller name can be referenced by `--disk` with the `on=<name>`
509option to attach disks to this controller.
510
511options:
512 `id=<name>` controller name (required)
513 `sub_channels=<N>` number of sub-channels (default 0)
514 `vtl2` assign to VTL2 (default VTL0)
515
516Examples:
517 --vmbus-scsi id=scsi0
518 --vmbus-scsi id=scsi1,sub_channels=4
519"#)]
520 #[clap(long = "vmbus-scsi")]
521 pub vmbus_scsi: Vec<ScsiControllerCli>,
522
523 #[clap(long_help = r#"
525Register an OpenHCL-managed storage controller that can be used as a
526relay target with `--disk ... relay=<name>`.
527
528syntax: id=<name>,type=scsi|nvme[,guid=<guid>]
529
530options:
531 `id=<name>` controller name (required)
532 `type=scsi|nvme` controller protocol (required)
533 `guid=<guid>` instance GUID (auto-derived from name if omitted)
534
535Examples:
536 --openhcl-controller id=vtl0-scsi,type=scsi
537 --openhcl-controller id=vtl0-nvme,type=nvme,guid=09a59b81-...
538"#)]
539 #[clap(long = "openhcl-controller")]
540 pub openhcl_controller: Vec<OpenhclControllerCli>,
541
542 #[clap(long = "cxl-test", value_name = "mem:<len>,pcie_port=<name>")]
544 pub cxl_test: Vec<CxlTestDeviceCli>,
545
546 #[clap(long_help = r#"
548e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
549
550syntax: <path> | kind:<arg>[,flag,opt=arg,...]
551
552valid disk kinds:
553 `mem:<len>` memory backed disk
554 <len>: length of ramdisk, e.g.: `1G`
555 `memdiff:<disk>` memory backed diff disk
556 <disk>: lower disk, e.g.: `file:base.img`
557 `file:<path>[;direct]` file-backed disk
558 <path>: path to file
559 `;direct`: bypass the OS page cache
560
561flags:
562 `ro` open disk as read-only
563
564options:
565 `pcie_port=<name>` present the disk using pcie under the specified port
566 `serial=<value>` 1-20 printable ASCII bytes except comma and brackets
567"#)]
568 #[clap(long = "virtio-blk")]
569 pub virtio_blk: Vec<DiskCli>,
570
571 #[cfg(target_os = "linux")]
596 #[clap(long = "vhost-user")]
597 pub vhost_user: Vec<VhostUserCli>,
598
599 #[clap(long, value_name = "COUNT", default_value = "0")]
601 pub scsi_sub_channels: u16,
602
603 #[clap(long)]
605 pub nic: bool,
606
607 #[clap(long)]
619 pub net: Vec<NicConfigCli>,
620
621 #[clap(long, value_name = "SWITCH_ID")]
625 pub kernel_vmnic: Vec<String>,
626
627 #[clap(long)]
629 pub gfx: bool,
630
631 #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
633 pub vtl2_gfx: bool,
634
635 #[clap(flatten)]
637 pub vnc: VncCli,
638
639 #[cfg(guest_arch = "x86_64")]
641 #[clap(long, default_value_t)]
642 pub apic_id_offset: u32,
643
644 #[clap(long)]
646 pub vps_per_socket: Option<u32>,
647
648 #[clap(long, default_value = "auto")]
650 pub smt: SmtConfigCli,
651
652 #[cfg(guest_arch = "x86_64")]
654 #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
655 pub x2apic: X2ApicConfig,
656
657 #[cfg(guest_arch = "aarch64")]
659 #[clap(long, default_value = "auto")]
660 pub gic_msi: GicMsiCli,
661
662 #[cfg(guest_arch = "aarch64")]
666 #[clap(long, value_name = "SMMU_CONFIG")]
667 pub smmu: Vec<SmmuCli>,
668
669 #[clap(long, value_name = "SERIAL")]
681 pub com1: Option<ComSerialConfigCli>,
682
683 #[clap(long, value_name = "SERIAL")]
686 pub com2: Option<ComSerialConfigCli>,
687
688 #[clap(long, value_name = "SERIAL")]
691 pub com3: Option<ComSerialConfigCli>,
692
693 #[clap(long, value_name = "SERIAL")]
696 pub com4: Option<ComSerialConfigCli>,
697
698 #[structopt(long, value_name = "SERIAL")]
700 pub vmbus_com1_serial: Option<SerialConfigCli>,
701
702 #[structopt(long, value_name = "SERIAL")]
704 pub vmbus_com2_serial: Option<SerialConfigCli>,
705
706 #[clap(long)]
708 pub serial_tx_only: bool,
709
710 #[clap(long, value_name = "SERIAL")]
712 pub debugcon: Option<DebugconSerialConfigCli>,
713
714 #[clap(
716 long,
717 short = 'e',
718 value_name = "OPTIONS",
719 num_args = 0..=1,
720 default_missing_value = ""
721 )]
722 pub uefi: Option<UefiCli>,
723
724 #[clap(
726 long = "uefi-firmware",
727 hide = true,
728 requires("uefi"),
729 conflicts_with("igvm"),
730 value_name = "FILE"
731 )]
732 pub deprecated_uefi_firmware: Option<PathBuf>,
733
734 #[clap(long = "uefi-debug", hide = true, requires("uefi"))]
736 pub deprecated_uefi_debug: bool,
737
738 #[clap(long = "uefi-enable-memory-protections", hide = true, requires("uefi"))]
740 pub deprecated_uefi_enable_memory_protections: bool,
741
742 #[clap(long = "uefi-force-dma-bounce", hide = true, requires("uefi"))]
744 pub deprecated_uefi_force_dma_bounce: bool,
745
746 #[clap(long = "uefi-force-firmware-version", hide = true, requires("uefi"))]
748 pub deprecated_uefi_force_firmware_version: bool,
749
750 #[clap(long, requires("pcat"))]
761 pub pcat_boot_order: Option<PcatBootOrderCli>,
762
763 #[clap(long, conflicts_with("uefi"))]
765 pub pcat: bool,
766
767 #[clap(long, requires("pcat"), value_name = "FILE")]
769 pub pcat_firmware: Option<PathBuf>,
770
771 #[clap(
773 long,
774 conflicts_with_all = ["kernel", "pcat"],
775 value_name = "OPTIONS",
776 long_help = r#"Boot an IGVM file.
777
778Syntax: --igvm firmware=<FILE>,personality=<openhcl|uefi|linux-direct>
779
780Both firmware and personality are required; neither has a default.
781The openhcl personality requires explicit --hv --vtl2."#
782 )]
783 pub igvm: Option<IgvmCli>,
784
785 #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
788 pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
789
790 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
795 pub virtio_9p: Vec<FsArgs>,
796
797 #[clap(long)]
799 pub virtio_9p_debug: bool,
800
801 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
806 pub virtio_fs: Vec<FsArgsWithOptions>,
807
808 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
813 pub virtio_fs_shmem: Vec<FsArgs>,
814
815 #[clap(long, value_name = "BUS", default_value = "auto")]
817 pub virtio_fs_bus: VirtioBusCli,
818
819 #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
824 pub virtio_pmem: Option<VirtioPmemArgs>,
825
826 #[clap(long)]
828 pub virtio_rng: bool,
829
830 #[clap(long, value_name = "BUS", default_value = "auto")]
832 pub virtio_rng_bus: VirtioBusCli,
833
834 #[clap(long, value_name = "PORT", requires("virtio_rng"))]
836 pub virtio_rng_pcie_port: Option<String>,
837
838 #[clap(
841 long,
842 value_name = "OPTIONS",
843 num_args = 0..=1,
844 default_missing_value = ""
845 )]
846 pub virtio_rtc: Option<VirtioRtcCli>,
847
848 #[clap(long)]
854 pub virtio_console: Option<SerialConfigCli>,
855
856 #[clap(long, value_name = "PORT", requires("virtio_console"))]
858 pub virtio_console_pcie_port: Option<String>,
859
860 #[clap(long, value_name = "BUS", value_parser = parse_virtio_vsock_bus)]
862 pub virtio_vsock_bus: Option<VirtioBusCli>,
863
864 #[clap(long, value_name = "PATH")]
866 pub virtio_vsock_path: Option<String>,
867
868 #[cfg(target_os = "linux")]
871 #[clap(
872 long,
873 value_name = "CID",
874 conflicts_with = "virtio_vsock_path",
875 value_parser = parse_vhost_vsock_cid
876 )]
877 pub virtio_vsock_vhost_cid: Option<u32>,
878
879 #[clap(long)]
886 pub virtio_net: Vec<NicConfigCli>,
887
888 #[clap(long, value_name = "PATH")]
890 pub log_file: Option<PathBuf>,
891
892 #[clap(long, value_name = "PATH")]
896 pub pidfile: Option<PathBuf>,
897
898 #[clap(long, value_name = "SOCKETPATH")]
902 pub ttrpc: Option<PathBuf>,
903
904 #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
908 pub grpc: Option<PathBuf>,
909
910 #[clap(long_help = r#"
912Run as an RPC server on the specified Unix socket.
913
914syntax: path=<PATH>[,transport=<TRANSPORT>]
915
916options:
917 `path=<PATH>` Unix socket path to listen on (required)
918 `transport=<TRANSPORT>` wire transport to accept (default: auto)
919
920valid transports:
921 `auto` auto-detect ttrpc vs. gRPC per connection
922 `ttrpc` accept ttrpc clients only
923 `grpc` accept gRPC clients only
924
925Examples:
926 --rpc path=/tmp/openvmm.sock
927 --rpc path=/tmp/openvmm.sock,transport=ttrpc
928"#)]
929 #[clap(
930 long,
931 value_name = "path=PATH[,transport=TRANSPORT]",
932 conflicts_with("ttrpc"),
933 conflicts_with("grpc")
934 )]
935 pub rpc: Option<RpcCli>,
936
937 #[clap(long)]
939 pub single_process: bool,
940
941 #[cfg(windows)]
943 #[clap(long, value_name = "PATH")]
944 pub device: Vec<String>,
945
946 #[clap(long = "disable-frontpage", hide = true, requires("uefi"))]
948 pub deprecated_disable_frontpage: bool,
949
950 #[clap(
952 long,
953 value_name = "VERSION",
954 num_args = 0..=1,
955 default_missing_value = "185"
956 )]
957 pub tpm: Option<TpmVersionCli>,
958
959 #[clap(long, default_value = "control", hide(true))]
963 #[expect(clippy::option_option)]
964 pub internal_worker: Option<Option<String>>,
965
966 #[clap(long, requires("vtl2"))]
968 pub vmbus_redirect: bool,
969
970 #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
972 pub vmbus_max_version: Option<u32>,
973
974 #[clap(long_help = r#"
978e.g: --vmgs memdiff:file:/path/to/file.vmgs
979
980syntax: <path> | kind:<arg>[,flag]
981
982valid disk kinds:
983 `mem:<len>` memory backed disk
984 <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
985 `memdiff:<disk>[;create=<len>]` memory backed diff disk
986 <disk>: lower disk, e.g.: `file:base.img`
987 `file:<path>` file-backed disk
988 <path>: path to file
989
990flags:
991 `fmt` reprovision the VMGS before boot
992 `fmt-on-fail` reprovision the VMGS before boot if it is corrupted
993"#)]
994 #[clap(long)]
995 pub vmgs: Option<VmgsCli>,
996
997 #[clap(long, requires("vmgs"))]
999 pub test_gsp_by_id: bool,
1000
1001 #[clap(long, requires("pcat"), value_name = "FILE")]
1003 pub vga_firmware: Option<PathBuf>,
1004
1005 #[clap(long)]
1007 pub secure_boot: bool,
1008
1009 #[clap(long)]
1011 pub secure_boot_template: Option<SecureBootTemplateCli>,
1012
1013 #[clap(long, value_name = "PATH")]
1015 pub custom_uefi_json: Option<PathBuf>,
1016
1017 #[clap(long, hide(true))]
1022 pub relay_console_path: Option<PathBuf>,
1023
1024 #[clap(long, hide(true))]
1028 pub relay_console_title: Option<String>,
1029
1030 #[clap(long, value_name = "PORT")]
1032 pub gdb: Option<u16>,
1033
1034 #[clap(long)]
1039 pub mana: Vec<NicConfigCli>,
1040
1041 #[clap(long)]
1056 pub hypervisor: Option<String>,
1057
1058 #[clap(long)]
1064 pub nested_virt: bool,
1065
1066 #[clap(long_help = r#"
1076e.g: --ide memdiff:file:/path/to/disk.vhd
1077
1078syntax: <path> | kind:<arg>[,flag,opt=arg,...]
1079
1080valid disk kinds:
1081 `mem:<len>` memory backed disk
1082 <len>: length of ramdisk, e.g.: `1G`
1083 `memdiff:<disk>` memory backed diff disk
1084 <disk>: lower disk, e.g.: `file:base.img`
1085 `file:<path>[;create=<len>]` file-backed disk
1086 <path>: path to file
1087 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
1088 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
1089 `blob:<type>:<url>` HTTP blob (read-only)
1090 <type>: `flat` or `vhd1`
1091 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
1092 <cipher>: `xts-aes-256`
1093
1094additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
1095this list is not exhaustive.
1096
1097flags:
1098 `ro` open disk as read-only
1099 `s` attach drive to secondary ide channel
1100 `dvd` specifies that device is cd/dvd and it is read_only
1101"#)]
1102 #[clap(long, value_name = "FILE", requires("pcat"))]
1103 pub ide: Vec<IdeDiskCli>,
1104
1105 #[clap(long_help = r#"
1108e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
1109
1110syntax: <path> | kind:<arg>[,flag,opt=arg,...]
1111
1112valid disk kinds:
1113 `mem:<len>` memory backed disk
1114 <len>: length of ramdisk, e.g.: `1G`
1115 `memdiff:<disk>` memory backed diff disk
1116 <disk>: lower disk, e.g.: `file:base.img`
1117 `file:<path>[;create=<len>]` file-backed disk
1118 <path>: path to file
1119 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
1120 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
1121 `blob:<type>:<url>` HTTP blob (read-only)
1122 <type>: `flat` or `vhd1`
1123 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
1124 <cipher>: `xts-aes-256`
1125
1126flags:
1127 `ro` open disk as read-only
1128"#)]
1129 #[clap(long, value_name = "FILE", requires("pcat"))]
1130 pub floppy: Vec<FloppyDiskCli>,
1131
1132 #[clap(long)]
1134 pub guest_watchdog: bool,
1135
1136 #[clap(long)]
1139 pub openhcl_dump_path: Option<PathBuf>,
1140
1141 #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action)]
1145 pub guest_reset_action: GuestPowerAction,
1146
1147 #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1151 pub guest_shutdown_action: GuestPowerAction,
1152
1153 #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1157 pub guest_crash_action: GuestPowerAction,
1158
1159 #[clap(long, value_name = "PATH")]
1167 pub crash_dump_path: Option<PathBuf>,
1168
1169 #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action, requires = "guest_watchdog")]
1173 pub guest_watchdog_action: GuestPowerAction,
1174
1175 #[clap(long)]
1177 pub write_saved_state_proto: Option<PathBuf>,
1178
1179 #[clap(long)]
1181 pub imc: Option<PathBuf>,
1182
1183 #[clap(long)]
1185 pub battery: bool,
1186
1187 #[clap(long)]
1189 pub hibernation: bool,
1190
1191 #[clap(long = "uefi-console-mode", hide = true, requires("uefi"))]
1193 pub deprecated_uefi_console_mode: Option<UefiConsoleModeCli>,
1194
1195 #[clap(long_help = r#"
1197Set the EFI diagnostics log level.
1198
1199options:
1200 default default (ERROR and WARN only)
1201 info info (ERROR, WARN, and INFO)
1202 full full (all log levels)
1203"#)]
1204 #[clap(long = "efi-diagnostics-log-level", hide = true, requires("uefi"))]
1205 pub deprecated_efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
1206
1207 #[clap(long = "default-boot-always-attempt", hide = true)]
1209 pub deprecated_default_boot_always_attempt: bool,
1210
1211 #[cfg(guest_arch = "x86_64")]
1217 #[clap(long)]
1218 pub amd_iommu: Vec<String>,
1219
1220 #[cfg(guest_arch = "x86_64")]
1226 #[clap(long)]
1227 pub intel_vtd: Vec<String>,
1228
1229 #[clap(long_help = r#"
1231Attach root complexes to the VM.
1232
1233Examples:
1234 # Attach root complex rc0 on segment 0 with bus and MMIO ranges
1235 --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
1236
1237 # Configure HDM window size and restrictions (bitmask)
1238 --pcie-root-complex rc1,hdm=2G,hdm_window_restrictions=0x21
1239
1240Syntax: <name>[,opt=arg,...]
1241
1242Options:
1243 `segment=<value>` configures the PCI Express segment, default 0
1244 `start_bus=<value>` lowest valid bus number, default 0
1245 `end_bus=<value>` highest valid bus number, default 255
1246 `low_mmio=<size>` low MMIO window size, default 64M
1247 `high_mmio=<size>` high MMIO window size, default 1G
1248 `low_mmio_base=<addr>` pin low MMIO window base address (0x-prefixed hex)
1249 `high_mmio_base=<addr>` pin high MMIO window base address (0x-prefixed hex)
1250 `hdm=<size>` HDM decoder MMIO window size (CFMWS window), default 1G
1251 `hdm_window_restrictions=<m>` CFMWS window restriction bitmask (u16, decimal or 0x-prefixed hex),
1252 default DEVICE_COHERENT (bit 0, value 0x1)
1253 `preserve_bars` keep pinned BARs at their assigned addresses
1254 `node=<value>` NUMA node the root complex is associated with
1255"#)]
1256 #[clap(long, conflicts_with("pcat"))]
1257 pub pcie_root_complex: Vec<PcieRootComplexCli>,
1258
1259 #[clap(long, requires("pcie_root_complex"), conflicts_with("pcat"))]
1261 pub pcie_ecam_below_4gb: bool,
1262
1263 #[clap(long_help = r#"
1265Attach root ports to root complexes.
1266
1267Examples:
1268 # Attach root port rc0rp0 to root complex rc0
1269 --pcie-root-port rc0:rc0rp0
1270
1271 # Attach root port rc0rp1 to root complex rc0 with hotplug support
1272 --pcie-root-port rc0:rc0rp1,hotplug
1273
1274 # Attach root port rc0rp2 at device 5, function 0
1275 --pcie-root-port rc0:rc0rp2,addr=5
1276
1277 # Attach root port rc0rp3 at device 5, function 1
1278 --pcie-root-port rc0:rc0rp3,addr=5.1
1279
1280Syntax: <root_complex_name>:<name>[,opt,opt=arg,...]
1281
1282Options:
1283 `addr=<dev>[.<fn>]` device/function to place this port at (default:
1284 lowest available); dev 0-31, fn 0-7
1285 `hotplug` enable hotplug support for this root port
1286 `acs=<mask>` ACS capability bitmask (u16, decimal or 0x-prefixed hex)
1287 `cxl` configure this root port as CXL-capable
1288 `pasid` configure this port to support PASID for downstream devices
1289"#)]
1290 #[clap(long, conflicts_with("pcat"))]
1291 pub pcie_root_port: Vec<PcieRootPortCli>,
1292
1293 #[clap(long_help = r#"
1295Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
1296
1297Examples:
1298 # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
1299 --pcie-switch rp0:switch0,num_downstream_ports=4
1300
1301 # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
1302 --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
1303
1304 # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
1305 --pcie-switch rp0:switch0
1306 --pcie-switch switch0-downstream-0:switch1
1307 --pcie-switch switch1-downstream-1:switch2
1308
1309 # Enable hotplug on all downstream switch ports of switch0
1310 --pcie-switch rp0:switch0,hotplug
1311
1312 # Enable PASID on all downstream switch ports of switch0
1313 --pcie-switch rp0:switch0,pasid
1314
1315Syntax: <port_name>:<name>[,opt,opt=arg,...]
1316
1317 port_name can be:
1318 - Root port name (e.g., "rp0") to connect directly to a root port
1319 - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
1320
1321Options:
1322 `hotplug` enable hotplug support for all downstream switch ports
1323 `num_downstream_ports=<value>` number of downstream ports, default 4
1324 `acs=<mask>` ACS capability bitmask for downstream switch ports
1325 `pasid` configure this port to support PASID for downstream devices
1326"#)]
1327 #[clap(long, conflicts_with("pcat"))]
1328 pub pcie_switch: Vec<GenericPcieSwitchCli>,
1329
1330 #[clap(long_help = r#"
1332Declare that the device directly behind a PCIe port is a generic initiator
1333(GI) for a NUMA node, generating an SRAT Generic Initiator Affinity structure.
1334
1335The port may be a root port or a switch downstream port, so this works for
1336devices that sit behind a switch (e.g. a GPU placed under a switch shared
1337with a NIC for peer-to-peer DMA). The port is resolved by name against the
1338live topology after switch downstream ports are enumerated.
1339
1340Examples:
1341 # The device behind switch downstream port sw1-downstream-0 is a generic
1342 # initiator for NUMA node 1
1343 --pcie-generic-initiator port=sw1-downstream-0,node=1
1344
1345 # Also works for a root port name
1346 --pcie-generic-initiator port=rp0,node=2
1347
1348Syntax: port=<port_name>,node=<node>
1349"#)]
1350 #[clap(
1351 long = "pcie-generic-initiator",
1352 value_name = "port=<name>,node=<node>",
1353 conflicts_with("pcat")
1354 )]
1355 pub pcie_generic_initiator: Vec<PcieGenericInitiatorCli>,
1356
1357 #[clap(long_help = r#"
1359Attach PCIe devices to root ports or downstream switch ports
1360which are implemented in a simulator running in a remote process.
1361
1362Examples:
1363 # Attach to root port rc0rp0 with default socket
1364 --pcie-remote rc0rp0
1365
1366 # Attach with custom socket address
1367 --pcie-remote rc0rp0,socket=0.0.0.0:48914
1368
1369 # Specify HU and controller identifiers
1370 --pcie-remote rc0rp0,hu=1,controller=0
1371
1372 # Multiple devices on different ports
1373 --pcie-remote rc0rp0,socket=0.0.0.0:48914
1374 --pcie-remote rc0rp1,socket=0.0.0.0:48915
1375
1376Syntax: <port_name>[,opt=arg,...]
1377
1378Options:
1379 `socket=<address>` TCP socket (default: localhost:48914)
1380 `hu=<value>` Hardware unit identifier (default: 0)
1381 `controller=<value>` Controller identifier (default: 0)
1382"#)]
1383 #[clap(long, conflicts_with("pcat"))]
1384 pub pcie_remote: Vec<PcieRemoteCli>,
1385
1386 #[clap(long_help = r#"
1388Assign a host PCI device to the guest via Linux VFIO.
1389
1390The device must be bound to vfio-pci on the host before starting the VM.
1391
1392Examples:
1393 --vfio host=0000:01:00.0,port=rp0
1394 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1395
1396Keys:
1397 host=<pci_bdf> (required) PCI address on the host
1398 port=<name> (required) Root port or downstream switch port name
1399 iommu=<id> (optional) Reference to an --iommu object. When present,
1400 uses VFIO cdev + iommufd instead of the legacy group path.
1401"#)]
1402 #[cfg(target_os = "linux")]
1403 #[clap(long, conflicts_with("pcat"))]
1404 pub vfio: Vec<VfioDeviceCli>,
1405
1406 #[clap(long_help = r#"
1408Declare an iommufd context. Opens /dev/iommu so it can be referenced by
1409--vfio devices via the iommu=<id> key. The associated IOAS is allocated
1410the first time a --vfio device referring to this id is opened.
1411
1412Requires Linux kernel >= 6.6 with iommufd support.
1413
1414Examples:
1415 --iommu id=iommu0 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1416
1417Syntax: id=<name>
1418"#)]
1419 #[cfg(target_os = "linux")]
1420 #[clap(long, conflicts_with("pcat"))]
1421 pub iommu: Vec<IommuCli>,
1422}
1423
1424impl Options {
1425 pub fn effective_uefi(&self) -> anyhow::Result<Option<UefiCli>> {
1427 let mut uefi = match &self.uefi {
1428 Some(uefi) => uefi.clone(),
1429 None => return Ok(None),
1430 };
1431
1432 if let Some(firmware) = &self.deprecated_uefi_firmware {
1433 if uefi.firmware.is_some() {
1434 anyhow::bail!("--uefi firmware=... conflicts with --uefi-firmware");
1435 }
1436 uefi.firmware = Some(firmware.clone());
1437 }
1438 if let Some(console) = self.deprecated_uefi_console_mode {
1439 if uefi.console.is_some() {
1440 anyhow::bail!("--uefi console=... conflicts with --uefi-console-mode");
1441 }
1442 uefi.console = Some(console);
1443 }
1444 if let Some(diagnostics) = self.deprecated_efi_diagnostics_log_level {
1445 if uefi.diagnostics.is_some() {
1446 anyhow::bail!("--uefi diagnostics=... conflicts with --efi-diagnostics-log-level");
1447 }
1448 uefi.diagnostics = Some(diagnostics);
1449 }
1450
1451 uefi.debug |= self.deprecated_uefi_debug;
1452 uefi.enable_memory_protections |= self.deprecated_uefi_enable_memory_protections;
1453 uefi.force_dma_bounce |= self.deprecated_uefi_force_dma_bounce;
1454 uefi.force_firmware_version |= self.deprecated_uefi_force_firmware_version;
1455 uefi.disable_frontpage |= self.deprecated_disable_frontpage;
1456 uefi.default_boot_always_attempt |= self.deprecated_default_boot_always_attempt;
1457 Ok(Some(uefi))
1458 }
1459
1460 pub fn memory_size(&self) -> u64 {
1462 self.memory.size.map(|m| m.0).unwrap_or(DEFAULT_MEMORY_SIZE)
1463 }
1464
1465 pub fn prefetch_memory(&self) -> bool {
1467 self.memory.prefetch || self.deprecated_prefetch
1468 }
1469
1470 pub fn private_memory(&self) -> bool {
1472 self.memory.shared == Some(false) || self.deprecated_private_memory
1473 }
1474
1475 pub fn transparent_hugepages(&self) -> bool {
1477 self.memory
1478 .transparent_hugepages
1479 .unwrap_or(!self.memory.hugepages)
1480 || self.deprecated_thp
1481 }
1482
1483 pub fn memory_backing_file(&self) -> Option<&PathBuf> {
1485 self.memory
1486 .file
1487 .as_ref()
1488 .or(self.deprecated_memory_backing_file.as_ref())
1489 }
1490
1491 pub fn validate_memory_options(&self) -> anyhow::Result<()> {
1499 if self.memory.file.is_some() && self.deprecated_memory_backing_file.is_some() {
1500 anyhow::bail!("--memory file=... conflicts with --memory-backing-file");
1501 }
1502 if self.memory.file.is_some() && self.restore_snapshot.is_some() {
1503 anyhow::bail!("--memory file=... conflicts with --restore-snapshot");
1504 }
1505 if self.memory.shared == Some(true) && self.deprecated_private_memory {
1506 anyhow::bail!("--memory shared=on conflicts with --private-memory");
1507 }
1508 Ok(())
1509 }
1510
1511 pub fn validate_isolation_options(&self) -> anyhow::Result<()> {
1513 if self.snp_restricted_injection && !matches!(self.isolation, Some(IsolationCli::Snp)) {
1514 anyhow::bail!("--snp-restricted-injection requires --isolation snp");
1515 }
1516 if self.snp_restricted_injection
1517 && self
1518 .hypervisor
1519 .as_deref()
1520 .and_then(|value| value.split(':').next())
1521 != Some("mshv")
1522 {
1523 anyhow::bail!("--snp-restricted-injection requires --hypervisor mshv");
1524 }
1525 if self.snp_restricted_injection
1526 && (self.uefi.is_some()
1527 || self.pcat
1528 || self.igvm.is_some()
1529 || self.restore_snapshot.is_some())
1530 {
1531 anyhow::bail!("--snp-restricted-injection requires Linux direct boot");
1532 }
1533 if matches!(self.isolation, Some(IsolationCli::Snp)) {
1534 if self.uefi.is_some() {
1535 anyhow::bail!("SNP isolation currently only supports Linux direct boot");
1536 }
1537 if self.memory.hugepages
1538 || self
1539 .numa
1540 .as_ref()
1541 .is_some_and(|nodes| nodes.iter().any(|node| node.memory.hugepages))
1542 {
1543 anyhow::bail!("SNP isolation currently does not support hugetlb memory");
1544 }
1545 }
1546 Ok(())
1547 }
1548
1549 pub fn validate_igvm_options(&self) -> anyhow::Result<()> {
1551 let Some(igvm) = &self.igvm else {
1552 return Ok(());
1553 };
1554 match igvm.personality {
1555 IgvmPersonalityCli::Openhcl => {
1556 anyhow::ensure!(
1557 self.hv && self.vtl2,
1558 "--igvm personality=openhcl requires --hv --vtl2"
1559 );
1560 }
1561 IgvmPersonalityCli::Uefi | IgvmPersonalityCli::LinuxDirect => {
1562 anyhow::ensure!(
1563 !self.vtl2,
1564 "--vtl2 requires --igvm personality=openhcl for IGVM boots"
1565 );
1566 anyhow::ensure!(
1567 self.uefi.is_none(),
1568 "--uefi is only supported with --igvm personality=openhcl"
1569 );
1570 }
1571 }
1572 Ok(())
1573 }
1574}
1575
1576#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
1577pub struct IgvmCli {
1578 pub firmware: PathBuf,
1579 pub personality: IgvmPersonalityCli,
1580}
1581
1582#[derive(Clone, Debug, Default, vmm_cli::KeyValueArgs)]
1583pub struct UefiCli {
1584 pub firmware: Option<PathBuf>,
1585 #[kv(flag)]
1586 pub debug: bool,
1587 #[kv(flag)]
1588 pub enable_memory_protections: bool,
1589 #[kv(flag)]
1590 pub force_dma_bounce: bool,
1591 #[kv(flag)]
1592 pub force_firmware_version: bool,
1593 #[kv(flag)]
1594 pub disable_frontpage: bool,
1595 pub console: Option<UefiConsoleModeCli>,
1596 pub diagnostics: Option<EfiDiagnosticsLogLevelCli>,
1597 #[kv(flag)]
1598 pub default_boot_always_attempt: bool,
1599}
1600
1601#[derive(Clone, Debug, PartialEq, Eq, vmm_cli::KeyValueArgs)]
1603pub struct VirtioRtcCli {
1604 #[kv(default = VirtioBusCli::Auto)]
1606 pub bus: VirtioBusCli,
1607}
1608
1609#[derive(Clone, Debug, PartialEq)]
1610pub struct FsArgs {
1611 pub tag: String,
1612 pub path: String,
1613 pub pcie_port: Option<String>,
1614}
1615
1616impl FromStr for FsArgs {
1617 type Err = anyhow::Error;
1618
1619 fn from_str(s: &str) -> Result<Self, Self::Err> {
1620 let (pcie_port, s) = parse_pcie_port_prefix(s);
1621 let mut s = s.split(',');
1622 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
1623 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
1624 };
1625 Ok(Self {
1626 tag: tag.to_owned(),
1627 path: path.to_owned(),
1628 pcie_port,
1629 })
1630 }
1631}
1632
1633#[derive(Clone, Debug, PartialEq)]
1634pub struct FsArgsWithOptions {
1635 pub tag: String,
1637 pub path: String,
1639 pub options: String,
1641 pub pcie_port: Option<String>,
1643}
1644
1645impl FromStr for FsArgsWithOptions {
1646 type Err = anyhow::Error;
1647
1648 fn from_str(s: &str) -> Result<Self, Self::Err> {
1649 let (pcie_port, s) = parse_pcie_port_prefix(s);
1650 let mut s = s.split(',');
1651 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
1652 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
1653 };
1654 let options = s.collect::<Vec<_>>().join(";");
1655 Ok(Self {
1656 tag: tag.to_owned(),
1657 path: path.to_owned(),
1658 options,
1659 pcie_port,
1660 })
1661 }
1662}
1663
1664#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1668pub enum GuestPowerAction {
1669 Reset,
1671 Halt,
1674 Exit(u8),
1676}
1677
1678fn parse_guest_power_action(s: &str) -> Result<GuestPowerAction, String> {
1681 match s {
1682 "reset" => Ok(GuestPowerAction::Reset),
1683 "halt" => Ok(GuestPowerAction::Halt),
1684 "exit" => Ok(GuestPowerAction::Exit(0)),
1685 _ => match s.strip_prefix("exit:") {
1686 Some(code) => code
1687 .parse::<u8>()
1688 .map(GuestPowerAction::Exit)
1689 .map_err(|err| format!("invalid exit code '{code}' (expected 0-255): {err}")),
1690 None => Err(format!(
1691 "expected reset, halt, exit, or exit:<code>, got '{s}'"
1692 )),
1693 },
1694 }
1695}
1696
1697#[derive(Clone, Debug, PartialEq, Eq)]
1698pub enum VirtioBusCli {
1699 Auto,
1700 Mmio,
1701 Pci,
1702 Pcie(String),
1703 Vpci,
1704}
1705
1706impl FromStr for VirtioBusCli {
1707 type Err = String;
1708
1709 fn from_str(value: &str) -> Result<Self, Self::Err> {
1710 parse_virtio_bus(value, false)
1711 }
1712}
1713
1714fn parse_virtio_bus(value: &str, ignore_case: bool) -> Result<VirtioBusCli, String> {
1715 let eq = |value: &str, expected: &str| {
1716 if ignore_case {
1717 value.eq_ignore_ascii_case(expected)
1718 } else {
1719 value == expected
1720 }
1721 };
1722
1723 match value.split_once(':') {
1724 None if eq(value, "auto") => Ok(VirtioBusCli::Auto),
1725 None if eq(value, "mmio") => Ok(VirtioBusCli::Mmio),
1726 None if eq(value, "pci") => Ok(VirtioBusCli::Pci),
1727 None if eq(value, "vpci") => Ok(VirtioBusCli::Vpci),
1728 Some((bus, port)) if eq(bus, "pcie") && !port.is_empty() && !port.contains(':') => {
1729 Ok(VirtioBusCli::Pcie(port.to_string()))
1730 }
1731 _ => Err("expected auto, mmio, pci, pcie:<port>, or vpci".to_string()),
1732 }
1733}
1734
1735fn parse_virtio_vsock_bus(value: &str) -> Result<VirtioBusCli, String> {
1736 if value.eq_ignore_ascii_case("pcie") {
1737 return Ok(VirtioBusCli::Pcie("vsock".to_string()));
1738 }
1739
1740 match parse_virtio_bus(value, true) {
1741 Ok(bus @ (VirtioBusCli::Mmio | VirtioBusCli::Pci | VirtioBusCli::Pcie(_))) => Ok(bus),
1742 _ => Err("expected mmio, pci, pcie, or pcie:<port>".to_string()),
1743 }
1744}
1745
1746#[cfg(target_os = "linux")]
1747fn parse_vhost_vsock_cid(value: &str) -> Result<u32, String> {
1748 let cid = value
1749 .parse::<u32>()
1750 .map_err(|error| format!("invalid CID '{value}': {error}"))?;
1751 if !(3..u32::MAX).contains(&cid) {
1752 return Err(format!("CID must be between 3 and {}", u32::MAX - 1));
1753 }
1754 Ok(cid)
1755}
1756
1757fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
1762 if let Some(rest) = s.strip_prefix("pcie_port=") {
1763 if let Some((port, rest)) = rest.split_once(':') {
1764 if !port.is_empty() {
1765 return (Some(port.to_string()), rest);
1766 }
1767 }
1768 }
1769 (None, s)
1770}
1771
1772#[derive(Clone, Debug, PartialEq)]
1773pub struct VirtioPmemArgs {
1774 pub path: String,
1775 pub pcie_port: Option<String>,
1776}
1777
1778impl FromStr for VirtioPmemArgs {
1779 type Err = anyhow::Error;
1780
1781 fn from_str(s: &str) -> Result<Self, Self::Err> {
1782 let (pcie_port, s) = parse_pcie_port_prefix(s);
1783 if s.is_empty() {
1784 anyhow::bail!("expected [pcie_port=<port>:]<path>");
1785 }
1786 Ok(Self {
1787 path: s.to_owned(),
1788 pcie_port,
1789 })
1790 }
1791}
1792
1793#[derive(clap::ValueEnum, Clone, Copy)]
1794pub enum SecureBootTemplateCli {
1795 Windows,
1796 UefiCa,
1797}
1798
1799#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
1801pub enum TpmVersionCli {
1802 #[value(name = "138", alias = "1.38")]
1804 V138,
1805 #[value(name = "185", alias = "1.85")]
1807 V185,
1808}
1809
1810fn parse_memory(s: &str) -> anyhow::Result<u64> {
1811 if s == "VMGS_DEFAULT" {
1812 Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
1813 } else {
1814 || -> Option<u64> {
1815 let mut b = s.as_bytes();
1816 if s.ends_with('B') {
1817 b = &b[..b.len() - 1]
1818 }
1819 if b.is_empty() {
1820 return None;
1821 }
1822 let multi = match b[b.len() - 1] as char {
1823 'T' => Some(1024 * 1024 * 1024 * 1024),
1824 'G' => Some(1024 * 1024 * 1024),
1825 'M' => Some(1024 * 1024),
1826 'K' => Some(1024),
1827 _ => None,
1828 };
1829 if multi.is_some() {
1830 b = &b[..b.len() - 1]
1831 }
1832 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
1833 n.checked_mul(multi.unwrap_or(1))
1834 }()
1835 .with_context(|| format!("invalid memory size '{0}'", s))
1836 }
1837}
1838
1839fn parse_address(s: &str) -> anyhow::Result<u64> {
1841 let hex = s
1842 .strip_prefix("0x")
1843 .or_else(|| s.strip_prefix("0X"))
1844 .with_context(|| format!("invalid address '{s}', expected a 0x-prefixed hex value"))?;
1845 u64::from_str_radix(hex, 16).with_context(|| format!("invalid address '{s}'"))
1846}
1847
1848fn parse_acs_capability_mask(value: &str) -> anyhow::Result<u16> {
1849 if let Some(hex) = value
1850 .strip_prefix("0x")
1851 .or_else(|| value.strip_prefix("0X"))
1852 {
1853 u16::from_str_radix(hex, 16).context("invalid ACS capability mask")
1854 } else {
1855 value.parse::<u16>().context("invalid ACS capability mask")
1856 }
1857}
1858
1859#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1862pub struct SmbiosRelease(pub u8, pub u8);
1863
1864impl FromStr for SmbiosRelease {
1865 type Err = anyhow::Error;
1866
1867 fn from_str(value: &str) -> anyhow::Result<Self> {
1868 let (major, minor) = value
1869 .split_once('.')
1870 .with_context(|| format!("invalid smbios release '{value}', expected MAJOR.MINOR"))?;
1871 let major = major
1872 .parse::<u8>()
1873 .with_context(|| format!("invalid smbios release major '{major}'"))?;
1874 let minor = minor
1875 .parse::<u8>()
1876 .with_context(|| format!("invalid smbios release minor '{minor}'"))?;
1877 Ok(SmbiosRelease(major, minor))
1878 }
1879}
1880
1881#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1884pub enum SmbiosUuid {
1885 Random,
1887 Fixed(Guid),
1889}
1890
1891impl FromStr for SmbiosUuid {
1892 type Err = anyhow::Error;
1893
1894 fn from_str(value: &str) -> anyhow::Result<Self> {
1895 if value == "random" {
1896 Ok(SmbiosUuid::Random)
1897 } else {
1898 let guid = value
1899 .parse::<Guid>()
1900 .with_context(|| format!("invalid smbios uuid '{value}'"))?;
1901 Ok(SmbiosUuid::Fixed(guid))
1902 }
1903 }
1904}
1905
1906#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
1909pub struct SmbiosBiosCli {
1910 pub vendor: Option<String>,
1912 pub version: Option<String>,
1914 #[kv(key = "date")]
1916 pub release_date: Option<String>,
1917 pub release: Option<SmbiosRelease>,
1919}
1920
1921#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
1924pub struct SmbiosSystemCli {
1925 pub manufacturer: Option<String>,
1927 #[kv(key = "product")]
1929 pub product_name: Option<String>,
1930 pub version: Option<String>,
1932 #[kv(key = "serial")]
1934 pub serial_number: Option<String>,
1935 #[kv(key = "sku")]
1937 pub sku_number: Option<String>,
1938 pub family: Option<String>,
1940 pub uuid: Option<SmbiosUuid>,
1942}
1943
1944#[derive(Debug, Clone, Default, PartialEq, Eq)]
1951pub struct SmbiosCli {
1952 pub bios: SmbiosBiosCli,
1954 pub system: SmbiosSystemCli,
1956}
1957
1958fn smbios_set_once<T>(slot: &mut Option<T>, key: &str, value: T) -> anyhow::Result<()> {
1960 anyhow::ensure!(slot.is_none(), "duplicate smbios option '{key}'");
1961 *slot = Some(value);
1962 Ok(())
1963}
1964
1965impl SmbiosCli {
1966 pub fn merge(&mut self, other: SmbiosCli) -> anyhow::Result<()> {
1969 let SmbiosCli {
1970 bios:
1971 SmbiosBiosCli {
1972 vendor,
1973 version: bios_version,
1974 release_date,
1975 release,
1976 },
1977 system:
1978 SmbiosSystemCli {
1979 manufacturer,
1980 product_name,
1981 version: system_version,
1982 serial_number,
1983 sku_number,
1984 family,
1985 uuid,
1986 },
1987 } = other;
1988 merge_smbios_field(&mut self.bios.vendor, vendor, 0, "vendor")?;
1989 merge_smbios_field(&mut self.bios.version, bios_version, 0, "version")?;
1990 merge_smbios_field(&mut self.bios.release_date, release_date, 0, "date")?;
1991 merge_smbios_field(&mut self.bios.release, release, 0, "release")?;
1992 merge_smbios_field(
1993 &mut self.system.manufacturer,
1994 manufacturer,
1995 1,
1996 "manufacturer",
1997 )?;
1998 merge_smbios_field(&mut self.system.product_name, product_name, 1, "product")?;
1999 merge_smbios_field(&mut self.system.version, system_version, 1, "version")?;
2000 merge_smbios_field(&mut self.system.serial_number, serial_number, 1, "serial")?;
2001 merge_smbios_field(&mut self.system.sku_number, sku_number, 1, "sku")?;
2002 merge_smbios_field(&mut self.system.family, family, 1, "family")?;
2003 merge_smbios_field(&mut self.system.uuid, uuid, 1, "uuid")?;
2004 Ok(())
2005 }
2006}
2007
2008fn merge_smbios_field<T>(
2011 dst: &mut Option<T>,
2012 src: Option<T>,
2013 typ: u8,
2014 key: &str,
2015) -> anyhow::Result<()> {
2016 if let Some(value) = src {
2017 smbios_set_once(dst, &format!("type={typ} {key}"), value)?;
2018 }
2019 Ok(())
2020}
2021
2022fn parse_smbios(s: &str) -> anyhow::Result<SmbiosCli> {
2029 let mut typ: Option<u8> = None;
2030 let mut rest = Vec::new();
2031 for part in s.split(',') {
2032 let (key, value) = part
2033 .split_once('=')
2034 .with_context(|| format!("invalid smbios option '{part}', expected key=value"))?;
2035 if key.is_empty() || value.is_empty() {
2036 anyhow::bail!("invalid smbios option '{part}', expected key=value");
2037 }
2038 if key == "type" {
2039 anyhow::ensure!(typ.is_none(), "duplicate smbios option 'type'");
2040 typ = Some(
2041 value
2042 .parse::<u8>()
2043 .with_context(|| format!("invalid smbios type '{value}'"))?,
2044 );
2045 } else {
2046 rest.push(part);
2047 }
2048 }
2049
2050 let typ = typ.context("smbios option requires 'type=N' (e.g. 'type=1')")?;
2051 let rest = rest.join(",");
2052 Ok(match typ {
2053 0 => SmbiosCli {
2054 bios: rest.parse()?,
2055 ..Default::default()
2056 },
2057 1 => SmbiosCli {
2058 system: rest.parse()?,
2059 ..Default::default()
2060 },
2061 other => anyhow::bail!("unsupported smbios type '{other}' (expected 0 or 1)"),
2062 })
2063}
2064
2065fn parse_memory_config(s: &str) -> anyhow::Result<MemoryCli> {
2066 let memory = if !s.contains('=') && !s.contains(',') {
2068 MemoryCli {
2069 size: Some(s.parse::<vmm_cli::MemorySize>()?),
2070 ..Default::default()
2071 }
2072 } else {
2073 s.parse::<MemoryCli>()?
2074 };
2075 memory.validate()?;
2076 Ok(memory)
2077}
2078
2079fn parse_numa_node(s: &str) -> anyhow::Result<NumaNodeCli> {
2080 let node: NumaNodeCli = s.parse()?;
2081 anyhow::ensure!(
2082 node.memory.size.is_some(),
2083 "numa node requires 'size' option"
2084 );
2085 anyhow::ensure!(
2086 node.memory.file.is_none(),
2087 "'file' is not supported in --numa"
2088 );
2089 node.memory.validate()?;
2090 Ok(node)
2091}
2092
2093fn parse_numa_distance(s: &str) -> anyhow::Result<NumaDistanceCli> {
2094 let parts: Vec<&str> = s.split(':').collect();
2095 anyhow::ensure!(
2096 parts.len() == 3,
2097 "expected SRC:DST:DISTANCE format, got '{s}'"
2098 );
2099 let src = parts[0].parse::<u32>().context("invalid source node")?;
2100 let dst = parts[1]
2101 .parse::<u32>()
2102 .context("invalid destination node")?;
2103 let distance = parts[2].parse::<u8>().context("invalid distance")?;
2104 anyhow::ensure!(
2105 distance >= 10,
2106 "distance must be >= 10 (10 = local), got {distance}"
2107 );
2108 Ok(NumaDistanceCli { src, dst, distance })
2109}
2110
2111fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
2113 match s.strip_prefix("0x") {
2114 Some(rest) => u64::from_str_radix(rest, 16),
2115 None => s.parse::<u64>(),
2116 }
2117}
2118
2119#[derive(Clone, Debug, PartialEq)]
2120pub enum DiskCliKind {
2121 Memory(u64),
2123 MemoryDiff(Box<DiskCliKind>),
2125 Sqlite {
2127 path: PathBuf,
2128 create_with_len: Option<u64>,
2129 },
2130 SqliteDiff {
2132 path: PathBuf,
2133 create: bool,
2134 disk: Box<DiskCliKind>,
2135 },
2136 AutoCacheSqlite {
2138 cache_path: String,
2139 key: Option<String>,
2140 disk: Box<DiskCliKind>,
2141 },
2142 PersistentReservationsWrapper(Box<DiskCliKind>),
2144 File {
2146 path: PathBuf,
2147 create_with_len: Option<u64>,
2148 direct: bool,
2149 },
2150 Blob {
2152 kind: BlobKind,
2153 url: String,
2154 },
2155 Crypt {
2157 cipher: DiskCipher,
2158 key_file: PathBuf,
2159 disk: Box<DiskCliKind>,
2160 },
2161 DelayDiskWrapper {
2163 delay_ms: u64,
2164 disk: Box<DiskCliKind>,
2165 },
2166}
2167
2168#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
2169pub enum DiskCipher {
2170 #[clap(name = "xts-aes-256")]
2171 XtsAes256,
2172}
2173
2174#[derive(Copy, Clone, Debug, PartialEq)]
2175pub enum BlobKind {
2176 Flat,
2177 Vhd1,
2178}
2179
2180struct FileOpts {
2181 path: PathBuf,
2182 create_with_len: Option<u64>,
2183 direct: bool,
2184}
2185
2186fn parse_file_opts(arg: &str) -> anyhow::Result<FileOpts> {
2187 let mut path = arg;
2188 let mut create_with_len = None;
2189 let mut direct = false;
2190
2191 if let Some((p, rest)) = arg.split_once(';') {
2193 path = p;
2194 for opt in rest.split(';') {
2195 if let Some(len) = opt.strip_prefix("create=") {
2196 create_with_len = Some(parse_memory(len)?);
2197 } else if opt == "direct" {
2198 direct = true;
2199 } else {
2200 anyhow::bail!("invalid file option '{opt}', expected 'create=<len>' or 'direct'");
2201 }
2202 }
2203 }
2204
2205 Ok(FileOpts {
2206 path: path.into(),
2207 create_with_len,
2208 direct,
2209 })
2210}
2211
2212impl DiskCliKind {
2213 fn parse_autocache(
2216 arg: &str,
2217 cache_path: Result<String, std::env::VarError>,
2218 ) -> anyhow::Result<Self> {
2219 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
2220 let cache_path = cache_path.context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
2221 Ok(DiskCliKind::AutoCacheSqlite {
2222 cache_path,
2223 key: (!key.is_empty()).then(|| key.to_string()),
2224 disk: Box::new(kind.parse()?),
2225 })
2226 }
2227}
2228
2229impl FromStr for DiskCliKind {
2230 type Err = anyhow::Error;
2231
2232 fn from_str(s: &str) -> anyhow::Result<Self> {
2233 let disk = match s.split_once(':') {
2234 None => {
2236 let FileOpts {
2237 path,
2238 create_with_len,
2239 direct,
2240 } = parse_file_opts(s)?;
2241 DiskCliKind::File {
2242 path,
2243 create_with_len,
2244 direct,
2245 }
2246 }
2247 Some((kind, arg)) => match kind {
2248 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
2249 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
2250 "sql" => {
2251 let FileOpts {
2252 path,
2253 create_with_len,
2254 direct,
2255 } = parse_file_opts(arg)?;
2256 if direct {
2257 anyhow::bail!("'direct' is not supported for 'sql' disks");
2258 }
2259 DiskCliKind::Sqlite {
2260 path,
2261 create_with_len,
2262 }
2263 }
2264 "sqldiff" => {
2265 let (path_and_opts, kind) =
2266 arg.split_once(':').context("expected path[;opts]:kind")?;
2267 let disk = Box::new(kind.parse()?);
2268 match path_and_opts.split_once(';') {
2269 Some((path, create)) => {
2270 if create != "create" {
2271 anyhow::bail!("invalid syntax after ';', expected 'create'")
2272 }
2273 DiskCliKind::SqliteDiff {
2274 path: path.into(),
2275 create: true,
2276 disk,
2277 }
2278 }
2279 None => DiskCliKind::SqliteDiff {
2280 path: path_and_opts.into(),
2281 create: false,
2282 disk,
2283 },
2284 }
2285 }
2286 "autocache" => {
2287 Self::parse_autocache(arg, std::env::var("OPENVMM_AUTO_CACHE_PATH"))?
2288 }
2289 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
2290 "delay" => {
2291 let (delay_ms, kind) = arg.split_once(':').context("expected delay_ms:kind")?;
2292 DiskCliKind::DelayDiskWrapper {
2293 delay_ms: delay_ms.parse().context("invalid disk delay")?,
2294 disk: Box::new(kind.parse()?),
2295 }
2296 }
2297 "file" => {
2298 let FileOpts {
2299 path,
2300 create_with_len,
2301 direct,
2302 } = parse_file_opts(arg)?;
2303 DiskCliKind::File {
2304 path,
2305 create_with_len,
2306 direct,
2307 }
2308 }
2309 "blob" => {
2310 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
2311 let blob_kind = match blob_kind {
2312 "flat" => BlobKind::Flat,
2313 "vhd1" => BlobKind::Vhd1,
2314 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
2315 };
2316 DiskCliKind::Blob {
2317 kind: blob_kind,
2318 url: url.to_string(),
2319 }
2320 }
2321 "crypt" => {
2322 let (cipher, (key, kind)) = arg
2323 .split_once(':')
2324 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
2325 .context("expected cipher:key_file:kind")?;
2326 DiskCliKind::Crypt {
2327 cipher: ValueEnum::from_str(cipher, false)
2328 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
2329 key_file: PathBuf::from(key),
2330 disk: Box::new(kind.parse()?),
2331 }
2332 }
2333 kind => {
2334 let FileOpts {
2339 path,
2340 create_with_len,
2341 direct,
2342 } = parse_file_opts(s)?;
2343 if path.has_root() {
2344 DiskCliKind::File {
2345 path,
2346 create_with_len,
2347 direct,
2348 }
2349 } else {
2350 anyhow::bail!("invalid disk kind {kind}");
2351 }
2352 }
2353 },
2354 };
2355 Ok(disk)
2356 }
2357}
2358
2359#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2361pub enum RpcTransportCli {
2362 #[default]
2365 Auto,
2366 Ttrpc,
2368 Grpc,
2370}
2371
2372#[derive(Clone, Debug, PartialEq, Eq)]
2374pub struct RpcCli {
2375 pub path: PathBuf,
2377 pub transport: RpcTransportCli,
2379}
2380
2381impl FromStr for RpcCli {
2382 type Err = anyhow::Error;
2383
2384 fn from_str(s: &str) -> anyhow::Result<Self> {
2385 let mut path = None;
2386 let mut transport = None;
2387 for part in s.split(',') {
2388 let (key, value) = part
2389 .split_once('=')
2390 .with_context(|| format!("invalid rpc option '{part}', expected key=value"))?;
2391 match key {
2392 "path" => {
2393 anyhow::ensure!(path.is_none(), "duplicate option 'path'");
2394 anyhow::ensure!(!value.is_empty(), "'path' requires a value");
2395 path = Some(PathBuf::from(value));
2396 }
2397 "transport" => {
2398 anyhow::ensure!(transport.is_none(), "duplicate option 'transport'");
2399 transport = Some(match value {
2400 "auto" => RpcTransportCli::Auto,
2401 "ttrpc" => RpcTransportCli::Ttrpc,
2402 "grpc" => RpcTransportCli::Grpc,
2403 _ => anyhow::bail!(
2404 "invalid transport '{value}', expected auto, ttrpc, or grpc"
2405 ),
2406 });
2407 }
2408 _ => anyhow::bail!("unknown rpc option '{key}'"),
2409 }
2410 }
2411
2412 Ok(RpcCli {
2413 path: path.context("'path' is required")?,
2414 transport: transport.unwrap_or_default(),
2415 })
2416 }
2417}
2418
2419#[derive(Clone)]
2420pub struct VmgsCli {
2421 pub kind: DiskCliKind,
2422 pub provision: ProvisionVmgs,
2423}
2424
2425#[derive(Copy, Clone)]
2426pub enum ProvisionVmgs {
2427 OnEmpty,
2428 OnFailure,
2429 True,
2430}
2431
2432impl FromStr for VmgsCli {
2433 type Err = anyhow::Error;
2434
2435 fn from_str(s: &str) -> anyhow::Result<Self> {
2436 let (kind, opt) = s
2437 .split_once(',')
2438 .map(|(k, o)| (k, Some(o)))
2439 .unwrap_or((s, None));
2440 let kind = kind.parse()?;
2441
2442 let provision = match opt {
2443 None => ProvisionVmgs::OnEmpty,
2444 Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
2445 Some("fmt") => ProvisionVmgs::True,
2446 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
2447 };
2448
2449 Ok(VmgsCli { kind, provision })
2450 }
2451}
2452
2453#[derive(clap::Args)]
2455pub struct VncCli {
2456 #[clap(long)]
2458 pub vnc: bool,
2459
2460 #[clap(long, value_name = "PORT", default_value = "5900")]
2462 pub vnc_port: u16,
2463
2464 #[clap(long, value_name = "ADDRESS", default_value = "127.0.0.1")]
2468 pub vnc_listen: String,
2469
2470 #[clap(long, value_name = "COUNT", default_value = "16")]
2472 pub vnc_max_clients: usize,
2473
2474 #[clap(long)]
2477 pub vnc_evict_oldest: bool,
2478}
2479
2480#[derive(Clone)]
2482pub struct DiskCli {
2483 pub vtl: DeviceVtl,
2484 pub kind: DiskCliKind,
2485 pub read_only: bool,
2486 pub is_dvd: bool,
2487 pub underhill: Option<UnderhillDiskSource>,
2488 pub pcie_port: Option<String>,
2489 pub serial: Option<String>,
2490 pub controller: Option<String>,
2491 pub nsid: Option<u32>,
2492 pub lun: Option<u8>,
2493 pub relay: Option<(String, Option<u32>)>,
2494}
2495
2496#[derive(Copy, Clone)]
2497pub enum UnderhillDiskSource {
2498 Scsi,
2499 Nvme,
2500}
2501
2502struct RelayTarget {
2504 name: String,
2505 location: Option<u32>,
2506}
2507
2508impl FromStr for RelayTarget {
2509 type Err = anyhow::Error;
2510
2511 fn from_str(s: &str) -> anyhow::Result<Self> {
2512 Ok(if let Some((name, loc)) = s.split_once(':') {
2513 RelayTarget {
2514 name: name.to_string(),
2515 location: Some(loc.parse::<u32>().context("invalid relay location")?),
2516 }
2517 } else {
2518 RelayTarget {
2519 name: s.to_string(),
2520 location: None,
2521 }
2522 })
2523 }
2524}
2525
2526#[derive(vmm_cli::KeyValueArgs)]
2529struct DiskArgs {
2530 #[kv(positional)]
2531 kind: DiskCliKind,
2532 #[kv(flag)]
2533 ro: bool,
2534 #[kv(flag)]
2535 dvd: bool,
2536 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2537 vtl: DeviceVtl,
2538 #[kv(flag)]
2539 uh: bool,
2540 #[kv(flag, key = "uh-nvme")]
2541 uh_nvme: bool,
2542 pcie_port: Option<String>,
2543 serial: Option<String>,
2544 #[kv(key = "on")]
2545 controller: Option<String>,
2546 nsid: Option<u32>,
2547 lun: Option<u8>,
2548 relay: Option<RelayTarget>,
2549}
2550
2551impl FromStr for DiskCli {
2552 type Err = anyhow::Error;
2553
2554 fn from_str(s: &str) -> anyhow::Result<Self> {
2555 let args: DiskArgs = s.parse()?;
2556
2557 let underhill = match (args.uh, args.uh_nvme) {
2558 (false, false) => None,
2559 (true, false) => Some(UnderhillDiskSource::Scsi),
2560 (false, true) => Some(UnderhillDiskSource::Nvme),
2561 (true, true) => anyhow::bail!("`uh` and `uh-nvme` are mutually exclusive"),
2562 };
2563 let read_only = args.ro || args.dvd;
2564 let is_dvd = args.dvd;
2565 let vtl = args.vtl;
2566 let pcie_port = args.pcie_port;
2567 let serial = args.serial;
2568 let controller = args.controller;
2569 let nsid = args.nsid;
2570 let lun = args.lun;
2571 let relay = args.relay.map(|r| (r.name, r.location));
2572
2573 if serial.as_ref().is_some_and(|serial| {
2574 serial.bytes().any(|byte| {
2575 !(byte.is_ascii_graphic() || byte == b' ') || matches!(byte, b'[' | b']')
2576 })
2577 }) {
2578 anyhow::bail!("`serial` must contain only printable ASCII characters and no brackets");
2579 }
2580
2581 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
2582 anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
2583 }
2584
2585 if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
2586 anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
2587 }
2588
2589 if controller.is_some() && pcie_port.is_some() {
2590 anyhow::bail!("`on` is incompatible with `pcie_port`");
2591 }
2592
2593 if controller.is_some() && vtl != DeviceVtl::Vtl0 {
2594 anyhow::bail!(
2595 "`vtl2` is incompatible with `on`; the controller's VTL determines placement"
2596 );
2597 }
2598
2599 if controller.is_some() && underhill.is_some() {
2600 anyhow::bail!("`on` is incompatible with `uh` and `uh-nvme`; use `relay` instead");
2601 }
2602
2603 if nsid.is_some() && controller.is_none() {
2604 anyhow::bail!("`nsid` requires `on`");
2605 }
2606
2607 if lun.is_some() && controller.is_none() {
2608 anyhow::bail!("`lun` requires `on`");
2609 }
2610
2611 if nsid.is_some() && lun.is_some() {
2612 anyhow::bail!("`nsid` and `lun` are mutually exclusive");
2613 }
2614
2615 if relay.is_some() && controller.is_none() {
2616 anyhow::bail!("`relay` requires `on`");
2617 }
2618
2619 if relay.is_some() && underhill.is_some() {
2620 anyhow::bail!("`relay` is incompatible with `uh` and `uh-nvme`");
2621 }
2622
2623 Ok(DiskCli {
2624 vtl,
2625 kind: args.kind,
2626 read_only,
2627 is_dvd,
2628 underhill,
2629 pcie_port,
2630 serial,
2631 controller,
2632 nsid,
2633 lun,
2634 relay,
2635 })
2636 }
2637}
2638
2639#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueGroup)]
2641pub enum NvmeControllerTransport {
2642 #[kv(key = "pcie_port")]
2644 Pcie(String),
2645 #[kv(key = "vpci")]
2647 Vpci(Option<Guid>),
2648}
2649
2650#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2652pub struct NvmeControllerCli {
2653 pub id: String,
2655 #[kv(flatten)]
2657 pub transport: NvmeControllerTransport,
2658 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2660 pub vtl: DeviceVtl,
2661}
2662
2663#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2665pub struct ScsiControllerCli {
2666 pub id: String,
2668 #[kv(default)]
2670 pub sub_channels: u16,
2671 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2673 pub vtl: DeviceVtl,
2674}
2675
2676#[derive(Copy, Clone, Debug, PartialEq)]
2678pub enum OpenhclControllerType {
2679 Scsi,
2680 Nvme,
2681}
2682
2683#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2685pub struct OpenhclControllerCli {
2686 pub id: String,
2688 #[kv(key = "type")]
2690 pub controller_type: OpenhclControllerType,
2691 pub guid: Option<Guid>,
2693}
2694
2695impl FromStr for OpenhclControllerType {
2696 type Err = anyhow::Error;
2697
2698 fn from_str(s: &str) -> anyhow::Result<Self> {
2699 Ok(match s {
2700 "scsi" => OpenhclControllerType::Scsi,
2701 "nvme" => OpenhclControllerType::Nvme,
2702 other => anyhow::bail!("unknown controller type: '{other}'"),
2703 })
2704 }
2705}
2706
2707#[derive(Clone, Debug, PartialEq)]
2709pub struct CxlTestDeviceCli {
2710 pub hdm_size: u64,
2712 pub pcie_port: String,
2714}
2715
2716impl FromStr for CxlTestDeviceCli {
2717 type Err = anyhow::Error;
2718
2719 fn from_str(s: &str) -> anyhow::Result<Self> {
2720 let mut opts = s.split(',');
2721 let first = opts.next().context("expected CXL test device config")?;
2722 let (kind, arg) = first
2723 .split_once(':')
2724 .context("expected CXL test syntax: mem:<len>")?;
2725
2726 if kind != "mem" {
2727 anyhow::bail!("unsupported CXL test backing kind '{kind}', expected 'mem'");
2728 }
2729
2730 let hdm_size = parse_memory(arg).context("failed to parse CXL test HDM size")?;
2731 let mut pcie_port = None;
2732
2733 for opt in opts {
2734 let mut kv = opt.split('=');
2735 let key = kv.next().unwrap_or_default();
2736 match key {
2737 "pcie_port" => {
2738 let val = kv.next();
2739 if val.is_none_or(|v| v.is_empty()) {
2740 anyhow::bail!("`pcie_port` requires a port name");
2741 }
2742 pcie_port = Some(val.unwrap().to_string());
2743 }
2744 _ => anyhow::bail!("unknown option: '{opt}'"),
2745 }
2746 }
2747
2748 let Some(pcie_port) = pcie_port else {
2749 anyhow::bail!("`pcie_port=<name>` is required for `--cxl-test`");
2750 };
2751
2752 Ok(Self {
2753 hdm_size,
2754 pcie_port,
2755 })
2756 }
2757}
2758
2759#[derive(Clone)]
2761pub struct IdeDiskCli {
2762 pub kind: DiskCliKind,
2763 pub read_only: bool,
2764 pub channel: Option<u8>,
2765 pub device: Option<u8>,
2766 pub is_dvd: bool,
2767}
2768
2769impl FromStr for IdeDiskCli {
2770 type Err = anyhow::Error;
2771
2772 fn from_str(s: &str) -> anyhow::Result<Self> {
2773 let mut opts = s.split(',');
2774 let kind = opts.next().unwrap().parse()?;
2775
2776 let mut read_only = false;
2777 let mut channel = None;
2778 let mut device = None;
2779 let mut is_dvd = false;
2780 for opt in opts {
2781 let mut s = opt.split('=');
2782 let opt = s.next().unwrap();
2783 match opt {
2784 "ro" => read_only = true,
2785 "p" => channel = Some(0),
2786 "s" => channel = Some(1),
2787 "0" => device = Some(0),
2788 "1" => device = Some(1),
2789 "dvd" => {
2790 is_dvd = true;
2791 read_only = true;
2792 }
2793 _ => anyhow::bail!("unknown option: '{opt}'"),
2794 }
2795 }
2796
2797 Ok(IdeDiskCli {
2798 kind,
2799 read_only,
2800 channel,
2801 device,
2802 is_dvd,
2803 })
2804 }
2805}
2806
2807#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
2809pub struct FloppyDiskCli {
2810 #[kv(positional)]
2811 pub kind: DiskCliKind,
2812 #[kv(flag, key = "ro")]
2813 pub read_only: bool,
2814}
2815
2816#[derive(Clone)]
2817pub struct DebugconSerialConfigCli {
2818 pub port: u16,
2819 pub serial: SerialConfigCli,
2820}
2821
2822impl FromStr for DebugconSerialConfigCli {
2823 type Err = String;
2824
2825 fn from_str(s: &str) -> Result<Self, Self::Err> {
2826 let Some((port, serial)) = s.split_once(',') else {
2827 return Err("invalid format (missing comma between port and serial)".into());
2828 };
2829
2830 let port: u16 = parse_number(port)
2831 .map_err(|_| "could not parse port".to_owned())?
2832 .try_into()
2833 .map_err(|_| "port must be 16-bit")?;
2834 let serial: SerialConfigCli = serial.parse()?;
2835
2836 Ok(Self { port, serial })
2837 }
2838}
2839
2840#[derive(Clone, Debug, PartialEq)]
2843pub struct ComSerialConfigCli {
2844 pub debugger_mode: bool,
2846 pub backend: SerialConfigCli,
2848}
2849
2850impl FromStr for ComSerialConfigCli {
2851 type Err = String;
2852
2853 fn from_str(s: &str) -> Result<Self, Self::Err> {
2854 match s.strip_prefix("debugger-mode:") {
2855 Some(rest) => Ok(Self {
2856 debugger_mode: true,
2857 backend: rest.parse()?,
2858 }),
2859 None => Ok(Self {
2860 debugger_mode: false,
2861 backend: s.parse()?,
2862 }),
2863 }
2864 }
2865}
2866
2867#[derive(Clone, Debug, PartialEq)]
2869pub enum SerialConfigCli {
2870 None,
2871 Null,
2872 Console,
2873 NewConsole(Option<PathBuf>, Option<String>),
2874 Stderr,
2875 Pipe(PathBuf),
2876 Tcp(SocketAddr),
2877 File(PathBuf),
2878}
2879
2880impl FromStr for SerialConfigCli {
2881 type Err = String;
2882
2883 fn from_str(s: &str) -> Result<Self, Self::Err> {
2884 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
2885
2886 let first_key = match keyvalues.first() {
2887 Some(first_pair) => first_pair.0.as_str(),
2888 None => Err("invalid serial configuration: no values supplied")?,
2889 };
2890 let first_value = keyvalues.first().unwrap().1.as_ref();
2891
2892 let ret = match first_key {
2893 "none" => SerialConfigCli::None,
2894 "null" => SerialConfigCli::Null,
2895 "console" => SerialConfigCli::Console,
2896 "stderr" => SerialConfigCli::Stderr,
2897 "file" => match first_value {
2898 Some(path) => SerialConfigCli::File(path.into()),
2899 None => Err("invalid serial configuration: file requires a value")?,
2900 },
2901 "term" => {
2902 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
2904 let window_name = match window_name {
2905 Some((_, Some(name))) => Some(name.clone()),
2906 _ => None,
2907 };
2908
2909 SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
2910 }
2911 "listen" => match first_value {
2912 Some(path) => {
2913 if let Some(tcp) = path.strip_prefix("tcp:") {
2914 let addr = tcp
2915 .parse()
2916 .map_err(|err| format!("invalid tcp address: {err}"))?;
2917 SerialConfigCli::Tcp(addr)
2918 } else {
2919 SerialConfigCli::Pipe(path.into())
2920 }
2921 }
2922 None => Err(
2923 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
2924 )?,
2925 },
2926 _ => {
2927 return Err(format!(
2928 "invalid serial configuration: '{}' is not a known option",
2929 first_key
2930 ));
2931 }
2932 };
2933
2934 Ok(ret)
2935 }
2936}
2937
2938impl SerialConfigCli {
2939 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
2942 let mut ret = Vec::new();
2943
2944 for item in s.split(',') {
2946 let mut eqsplit = item.split('=');
2949 let key = eqsplit.next();
2950 let value = eqsplit.next();
2951
2952 if let Some(key) = key {
2953 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
2954 } else {
2955 return Err("invalid key=value pair in serial config".into());
2957 }
2958 }
2959 Ok(ret)
2960 }
2961}
2962
2963#[derive(Clone, Debug, PartialEq)]
2964pub enum EndpointConfigCli {
2965 None,
2966 Consomme {
2967 cidr: Option<String>,
2968 host_fwd: Vec<HostPortConfigCli>,
2969 },
2970 Dio {
2971 id: Option<String>,
2972 },
2973 Tap {
2974 name: String,
2975 },
2976}
2977
2978#[derive(Clone, Debug, PartialEq)]
2980pub struct HostPortConfigCli {
2981 pub protocol: HostPortProtocolCli,
2982 pub host_address: Option<std::net::IpAddr>,
2983 pub host_port: u16,
2984 pub guest_port: u16,
2985}
2986
2987#[derive(Clone, Debug, PartialEq)]
2989pub enum HostPortProtocolCli {
2990 Tcp,
2991 Udp,
2992}
2993
2994fn parse_hostfwd(s: &str) -> Result<HostPortConfigCli, String> {
2995 let (host_part, guest_part) = s.split_once('-').ok_or_else(|| {
2998 format!(
2999 "invalid hostfwd format '{s}', \
3000 expected 'proto:[hostaddr]:hostport-[guestaddr]:guestport'"
3001 )
3002 })?;
3003
3004 let (proto, host_addr_port) = host_part.split_once(':').ok_or_else(|| {
3006 format!("invalid hostfwd host part '{host_part}', expected 'proto:[hostaddr]:hostport'")
3007 })?;
3008 let protocol = match proto {
3009 "tcp" => HostPortProtocolCli::Tcp,
3010 "udp" => HostPortProtocolCli::Udp,
3011 other => {
3012 return Err(format!(
3013 "unknown hostfwd protocol '{other}', expected 'tcp' or 'udp'"
3014 ));
3015 }
3016 };
3017
3018 let (host_address, host_port) = parse_addr_port(host_addr_port)
3019 .map_err(|e| format!("invalid hostfwd host address/port: {e}"))?;
3020 let (_, guest_port) = parse_addr_port(guest_part)
3021 .map_err(|e| format!("invalid hostfwd guest address/port: {e}"))?;
3022
3023 Ok(HostPortConfigCli {
3024 protocol,
3025 host_address,
3026 host_port,
3027 guest_port,
3028 })
3029}
3030
3031fn parse_addr_port(s: &str) -> Result<(Option<std::net::IpAddr>, u16), String> {
3037 if let Some(rest) = s.strip_prefix('[') {
3038 let (addr, port) = rest
3040 .split_once("]:")
3041 .ok_or_else(|| format!("expected '[addr]:port', got '[{rest}'"))?;
3042 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
3043 let addr: std::net::IpAddr = addr
3044 .parse()
3045 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
3046 Ok((Some(addr), port))
3047 } else {
3048 match s.rsplit_once(':') {
3049 Some((addr, port)) => {
3050 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
3051 let addr = if addr.is_empty() {
3052 None
3053 } else {
3054 let parsed: std::net::IpAddr = addr
3055 .parse()
3056 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
3057 Some(parsed)
3058 };
3059 Ok((addr, port))
3060 }
3061 None => {
3062 let port: u16 = s.parse().map_err(|_| format!("invalid port '{s}'"))?;
3063 Ok((None, port))
3064 }
3065 }
3066 }
3067}
3068
3069impl FromStr for EndpointConfigCli {
3070 type Err = String;
3071
3072 fn from_str(s: &str) -> Result<Self, Self::Err> {
3073 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
3074 ["none"] => EndpointConfigCli::None,
3075 ["consomme", rest @ ..] => {
3076 let remaining = rest.join(":");
3077 let mut cidr = None;
3078 let mut host_fwd = Vec::new();
3079 for opt in remaining.split(',').filter(|s| !s.is_empty()) {
3080 if let Some(fwd) = opt.strip_prefix("hostfwd=") {
3081 host_fwd.push(parse_hostfwd(fwd)?);
3082 } else if cidr.is_none() {
3083 cidr = Some(opt.to_owned());
3084 } else {
3085 return Err(format!("unexpected consomme option '{opt}'"));
3086 }
3087 }
3088 EndpointConfigCli::Consomme { cidr, host_fwd }
3089 }
3090 ["dio", s @ ..] => EndpointConfigCli::Dio {
3091 id: s.first().map(|s| (*s).to_owned()),
3092 },
3093 ["tap", name] => EndpointConfigCli::Tap {
3094 name: (*name).to_owned(),
3095 },
3096 _ => return Err("invalid network backend".into()),
3097 };
3098
3099 Ok(ret)
3100 }
3101}
3102
3103#[derive(Clone, Debug, PartialEq)]
3104pub struct NicConfigCli {
3105 pub vtl: DeviceVtl,
3106 pub endpoint: EndpointConfigCli,
3107 pub max_queues: Option<u16>,
3108 pub underhill: bool,
3109 pub pcie_port: Option<String>,
3110}
3111
3112impl FromStr for NicConfigCli {
3113 type Err = String;
3114
3115 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
3116 let mut vtl = DeviceVtl::Vtl0;
3117 let mut max_queues = None;
3118 let mut underhill = false;
3119 let mut pcie_port = None;
3120 while let Some((opt, rest)) = s.split_once(':') {
3121 if let Some((opt, val)) = opt.split_once('=') {
3122 match opt {
3123 "queues" => {
3124 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
3125 }
3126 "pcie_port" => {
3127 if val.is_empty() {
3128 return Err("`pcie_port=` requires port name argument".into());
3129 }
3130 pcie_port = Some(val.to_string());
3131 }
3132 _ => break,
3133 }
3134 } else {
3135 match opt {
3136 "vtl2" => {
3137 vtl = DeviceVtl::Vtl2;
3138 }
3139 "uh" => underhill = true,
3140 _ => break,
3141 }
3142 }
3143 s = rest;
3144 }
3145
3146 if underhill && vtl != DeviceVtl::Vtl0 {
3147 return Err("`uh` is incompatible with `vtl2`".into());
3148 }
3149
3150 if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
3151 return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
3152 }
3153
3154 let endpoint = s.parse()?;
3155 Ok(NicConfigCli {
3156 vtl,
3157 endpoint,
3158 max_queues,
3159 underhill,
3160 pcie_port,
3161 })
3162 }
3163}
3164
3165#[derive(Debug, Error)]
3166#[error("unknown VTL2 relocation type: {0}")]
3167pub struct UnknownVtl2RelocationType(String);
3168
3169fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
3170 match s {
3171 "disable" => Ok(Vtl2BaseAddressType::File),
3172 s if s.starts_with("auto=") => {
3173 let s = s.strip_prefix("auto=").unwrap_or_default();
3174 let size = if s == "filesize" {
3175 None
3176 } else {
3177 let size = parse_memory(s).map_err(|e| {
3178 UnknownVtl2RelocationType(format!(
3179 "unable to parse memory size from {} for 'auto=' type, {e}",
3180 e
3181 ))
3182 })?;
3183 Some(size)
3184 };
3185 Ok(Vtl2BaseAddressType::MemoryLayout { size })
3186 }
3187 s if s.starts_with("absolute=") => {
3188 let s = s.strip_prefix("absolute=");
3189 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
3190 UnknownVtl2RelocationType(format!(
3191 "unable to parse number from {} for 'absolute=' type",
3192 e
3193 ))
3194 })?;
3195 Ok(Vtl2BaseAddressType::Absolute(addr))
3196 }
3197 s if s.starts_with("vtl2=") => {
3198 let s = s.strip_prefix("vtl2=").unwrap_or_default();
3199 let size = if s == "filesize" {
3200 None
3201 } else {
3202 let size = parse_memory(s).map_err(|e| {
3203 UnknownVtl2RelocationType(format!(
3204 "unable to parse memory size from {} for 'vtl2=' type, {e}",
3205 e
3206 ))
3207 })?;
3208 Some(size)
3209 };
3210 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
3211 }
3212 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
3213 }
3214}
3215
3216#[derive(Debug, Copy, Clone, PartialEq)]
3217pub enum SmtConfigCli {
3218 Auto,
3219 Force,
3220 Off,
3221}
3222
3223#[derive(Debug, Error)]
3224#[error("expected auto, force, or off")]
3225pub struct BadSmtConfig;
3226
3227impl FromStr for SmtConfigCli {
3228 type Err = BadSmtConfig;
3229
3230 fn from_str(s: &str) -> Result<Self, Self::Err> {
3231 let r = match s {
3232 "auto" => Self::Auto,
3233 "force" => Self::Force,
3234 "off" => Self::Off,
3235 _ => return Err(BadSmtConfig),
3236 };
3237 Ok(r)
3238 }
3239}
3240
3241#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
3242fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
3243 let r = match s {
3244 "auto" => X2ApicConfig::Auto,
3245 "supported" => X2ApicConfig::Supported,
3246 "off" => X2ApicConfig::Unsupported,
3247 "on" => X2ApicConfig::Enabled,
3248 _ => return Err("expected auto, supported, off, or on"),
3249 };
3250 Ok(r)
3251}
3252
3253#[derive(Debug, Copy, Clone, ValueEnum)]
3254pub enum Vtl0LateMapPolicyCli {
3255 Off,
3256 Log,
3257 Halt,
3258 Exception,
3259}
3260
3261#[derive(Debug, Copy, Clone, Default, ValueEnum)]
3263pub enum GicMsiCli {
3264 #[default]
3266 Auto,
3267 Its,
3269 V2m,
3271}
3272
3273#[derive(Debug, Copy, Clone, ValueEnum)]
3274pub enum IsolationCli {
3275 Vbs,
3276 Snp,
3277}
3278
3279#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
3280pub enum IgvmPersonalityCli {
3281 Openhcl,
3283 Uefi,
3285 LinuxDirect,
3287}
3288
3289impl FromStr for IgvmPersonalityCli {
3290 type Err = String;
3291
3292 fn from_str(value: &str) -> Result<Self, Self::Err> {
3293 ValueEnum::from_str(value, false)
3294 }
3295}
3296
3297#[derive(Debug, Copy, Clone, PartialEq)]
3298pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
3299
3300impl FromStr for PcatBootOrderCli {
3301 type Err = &'static str;
3302
3303 fn from_str(s: &str) -> Result<Self, Self::Err> {
3304 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
3305 let mut order = Vec::new();
3306
3307 for item in s.split(',') {
3308 let device = match item {
3309 "optical" => PcatBootDevice::Optical,
3310 "hdd" => PcatBootDevice::HardDrive,
3311 "net" => PcatBootDevice::Network,
3312 "floppy" => PcatBootDevice::Floppy,
3313 _ => return Err("unknown boot device type"),
3314 };
3315
3316 let default_pos = default_order
3317 .iter()
3318 .position(|x| x == &Some(device))
3319 .ok_or("cannot pass duplicate boot devices")?;
3320
3321 order.push(default_order[default_pos].take().unwrap());
3322 }
3323
3324 order.extend(default_order.into_iter().flatten());
3325 assert_eq!(order.len(), 4);
3326
3327 Ok(Self(order.try_into().unwrap()))
3328 }
3329}
3330
3331#[derive(Copy, Clone, Debug, ValueEnum)]
3332pub enum UefiConsoleModeCli {
3333 Default,
3334 Com1,
3335 Com2,
3336 None,
3337}
3338
3339impl FromStr for UefiConsoleModeCli {
3340 type Err = String;
3341
3342 fn from_str(value: &str) -> Result<Self, Self::Err> {
3343 ValueEnum::from_str(value, false)
3344 }
3345}
3346
3347#[derive(Copy, Clone, Debug, Default, ValueEnum)]
3348pub enum EfiDiagnosticsLogLevelCli {
3349 #[default]
3350 Default,
3351 Info,
3352 Full,
3353}
3354
3355impl FromStr for EfiDiagnosticsLogLevelCli {
3356 type Err = String;
3357
3358 fn from_str(value: &str) -> Result<Self, Self::Err> {
3359 ValueEnum::from_str(value, false)
3360 }
3361}
3362
3363#[derive(Clone, Debug, PartialEq)]
3364pub struct PcieRootComplexCli {
3365 pub name: String,
3366 pub segment: u16,
3367 pub start_bus: u8,
3368 pub end_bus: u8,
3369 pub low_mmio: u32,
3370 pub high_mmio: u64,
3371 pub low_mmio_base: Option<u64>,
3372 pub high_mmio_base: Option<u64>,
3373 pub preserve_bars: bool,
3374 pub hdm: u64,
3375 pub hdm_window_restrictions: CfmwsWindowRestrictions,
3376 pub vnode: Option<u32>,
3377}
3378
3379struct HexAddress(u64);
3381
3382impl FromStr for HexAddress {
3383 type Err = anyhow::Error;
3384
3385 fn from_str(s: &str) -> anyhow::Result<Self> {
3386 Ok(HexAddress(parse_address(s)?))
3387 }
3388}
3389
3390struct CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions);
3392
3393impl FromStr for CfmwsWindowRestrictionsCli {
3394 type Err = anyhow::Error;
3395
3396 fn from_str(s: &str) -> anyhow::Result<Self> {
3397 Ok(CfmwsWindowRestrictionsCli(
3398 parse_cxl_cfmws_window_restriction_u16_bitmask(s)?,
3399 ))
3400 }
3401}
3402
3403#[derive(vmm_cli::KeyValueArgs)]
3406struct PcieRootComplexArgs {
3407 #[kv(positional)]
3408 name: String,
3409 #[kv(default)]
3410 segment: u16,
3411 #[kv(default)]
3412 start_bus: u8,
3413 #[kv(default = 255)]
3414 end_bus: u8,
3415 #[kv(default = vmm_cli::MemorySize(64 * 1024 * 1024))]
3416 low_mmio: vmm_cli::MemorySize,
3417 #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
3418 high_mmio: vmm_cli::MemorySize,
3419 low_mmio_base: Option<HexAddress>,
3420 high_mmio_base: Option<HexAddress>,
3421 #[kv(flag)]
3422 preserve_bars: bool,
3423 #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
3424 hdm: vmm_cli::MemorySize,
3425 #[kv(default = CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions::DEVICE_COHERENT))]
3426 hdm_window_restrictions: CfmwsWindowRestrictionsCli,
3427 #[kv(key = "node")]
3428 vnode: Option<u32>,
3429}
3430
3431impl FromStr for PcieRootComplexCli {
3432 type Err = anyhow::Error;
3433
3434 fn from_str(s: &str) -> Result<Self, Self::Err> {
3435 let args: PcieRootComplexArgs = s.parse()?;
3436
3437 if args.start_bus > args.end_bus {
3438 anyhow::bail!("start_bus must be <= end_bus");
3439 }
3440
3441 let low_mmio = u32::try_from(args.low_mmio.0).context("low MMIO size exceeds 32 bits")?;
3442
3443 Ok(PcieRootComplexCli {
3444 name: args.name,
3445 segment: args.segment,
3446 start_bus: args.start_bus,
3447 end_bus: args.end_bus,
3448 low_mmio,
3449 high_mmio: args.high_mmio.0,
3450 low_mmio_base: args.low_mmio_base.map(|a| a.0),
3451 high_mmio_base: args.high_mmio_base.map(|a| a.0),
3452 preserve_bars: args.preserve_bars,
3453 hdm: args.hdm.0,
3454 hdm_window_restrictions: args.hdm_window_restrictions.0,
3455 vnode: args.vnode,
3456 })
3457 }
3458}
3459
3460fn parse_cxl_cfmws_window_restriction_u16_bitmask(
3461 s: &str,
3462) -> anyhow::Result<CfmwsWindowRestrictions> {
3463 let bits = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
3464 u16::from_str_radix(hex, 16).context("invalid hex bitmask")?
3465 } else {
3466 u16::from_str(s).context("invalid decimal bitmask")?
3467 };
3468
3469 CfmwsWindowRestrictions::try_from_bits(bits)
3470 .context("bitmask includes reserved CFMWS window restriction bits")
3471}
3472
3473#[derive(Clone, Debug, PartialEq)]
3474pub struct PcieRootPortCli {
3475 pub root_complex_name: String,
3476 pub name: String,
3477 pub devfn: Option<u8>,
3478 pub hotplug: bool,
3479 pub acs_capabilities_supported: Option<u16>,
3480 pub cxl: bool,
3481 pub pasid: bool,
3482}
3483
3484struct PortNamePair {
3487 parent: String,
3488 child: String,
3489}
3490
3491impl FromStr for PortNamePair {
3492 type Err = anyhow::Error;
3493
3494 fn from_str(s: &str) -> anyhow::Result<Self> {
3495 let mut it = s.split(':');
3496 let parent = it
3497 .next()
3498 .filter(|x| !x.is_empty())
3499 .context("expected parent name")?;
3500 let child = it
3501 .next()
3502 .filter(|x| !x.is_empty())
3503 .context("expected child name")?;
3504 anyhow::ensure!(it.next().is_none(), "unexpected token in '{s}'");
3505 Ok(PortNamePair {
3506 parent: parent.to_string(),
3507 child: child.to_string(),
3508 })
3509 }
3510}
3511
3512struct PcieAddr(u8);
3514
3515impl FromStr for PcieAddr {
3516 type Err = anyhow::Error;
3517
3518 fn from_str(s: &str) -> anyhow::Result<Self> {
3519 Ok(PcieAddr(parse_pcie_addr(s)?))
3520 }
3521}
3522
3523struct AcsMask(u16);
3525
3526impl FromStr for AcsMask {
3527 type Err = anyhow::Error;
3528
3529 fn from_str(s: &str) -> anyhow::Result<Self> {
3530 Ok(AcsMask(parse_acs_capability_mask(s)?))
3531 }
3532}
3533
3534#[derive(vmm_cli::KeyValueArgs)]
3536struct RootPortArgs {
3537 #[kv(positional)]
3538 names: PortNamePair,
3539 addr: Option<PcieAddr>,
3540 #[kv(flag)]
3541 hotplug: bool,
3542 acs: Option<AcsMask>,
3543 #[kv(flag)]
3544 cxl: bool,
3545 #[kv(flag)]
3546 pasid: bool,
3547}
3548
3549impl FromStr for PcieRootPortCli {
3550 type Err = anyhow::Error;
3551
3552 fn from_str(s: &str) -> Result<Self, Self::Err> {
3553 let args: RootPortArgs = s.parse()?;
3554 Ok(PcieRootPortCli {
3555 root_complex_name: args.names.parent,
3556 name: args.names.child,
3557 devfn: args.addr.map(|a| a.0),
3558 hotplug: args.hotplug,
3559 acs_capabilities_supported: args.acs.map(|a| a.0),
3560 cxl: args.cxl,
3561 pasid: args.pasid,
3562 })
3563 }
3564}
3565
3566fn parse_pcie_addr(s: &str) -> anyhow::Result<u8> {
3570 let parse_int = |v: &str| -> anyhow::Result<u8> {
3571 if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) {
3572 u8::from_str_radix(hex, 16).context("invalid hex number")
3573 } else {
3574 v.parse().context("invalid number")
3575 }
3576 };
3577
3578 let mut parts = s.split('.');
3579 let device = parse_int(parts.next().context("expected device number")?)?;
3580 let function = match parts.next() {
3581 Some(f) => parse_int(f)?,
3582 None => 0,
3583 };
3584 if parts.next().is_some() {
3585 anyhow::bail!("unexpected token in addr '{s}'");
3586 }
3587 if device > 31 {
3588 anyhow::bail!("device number {device} out of range (0-31)");
3589 }
3590 if function > 7 {
3591 anyhow::bail!("function number {function} out of range (0-7)");
3592 }
3593 Ok((device << 3) | function)
3594}
3595
3596#[derive(Clone, Debug, PartialEq)]
3597pub struct GenericPcieSwitchCli {
3598 pub port_name: String,
3599 pub name: String,
3600 pub num_downstream_ports: u8,
3601 pub hotplug: bool,
3602 pub acs_capabilities_supported: Option<u16>,
3603 pub pasid: bool,
3604}
3605
3606#[derive(vmm_cli::KeyValueArgs)]
3608struct SwitchArgs {
3609 #[kv(positional)]
3610 names: PortNamePair,
3611 #[kv(default = 4)]
3612 num_downstream_ports: u8,
3613 #[kv(flag)]
3614 hotplug: bool,
3615 acs: Option<AcsMask>,
3616 #[kv(flag)]
3617 pasid: bool,
3618}
3619
3620impl FromStr for GenericPcieSwitchCli {
3621 type Err = anyhow::Error;
3622
3623 fn from_str(s: &str) -> Result<Self, Self::Err> {
3624 let args: SwitchArgs = s.parse()?;
3625 Ok(GenericPcieSwitchCli {
3626 port_name: args.names.parent,
3627 name: args.names.child,
3628 num_downstream_ports: args.num_downstream_ports,
3629 hotplug: args.hotplug,
3630 acs_capabilities_supported: args.acs.map(|a| a.0),
3631 pasid: args.pasid,
3632 })
3633 }
3634}
3635
3636#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3638pub struct PcieGenericInitiatorCli {
3639 #[kv(key = "port")]
3642 pub port_name: String,
3643 pub node: u32,
3645}
3646
3647#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3649pub struct PcieRemoteCli {
3650 #[kv(positional)]
3652 pub port_name: String,
3653 #[kv(key = "socket")]
3655 pub socket_addr: Option<String>,
3656 #[kv(default)]
3658 pub hu: u16,
3659 #[kv(default)]
3661 pub controller: u16,
3662}
3663
3664#[cfg(target_os = "linux")]
3668#[derive(Clone, Debug)]
3669pub struct VfioDeviceCli {
3670 pub port_name: String,
3672 pub pci_id: String,
3674 pub iommu: Option<String>,
3677 pub bar_addresses: [vfio_assigned_device_resources::BarAddressConfig; 6],
3679}
3680
3681#[cfg(target_os = "linux")]
3683struct BarAddressCli(vfio_assigned_device_resources::BarAddressConfig);
3684
3685#[cfg(target_os = "linux")]
3686impl FromStr for BarAddressCli {
3687 type Err = anyhow::Error;
3688
3689 fn from_str(s: &str) -> anyhow::Result<Self> {
3690 let config = if s == "host" {
3691 vfio_assigned_device_resources::BarAddressConfig::HostAssigned
3692 } else if let Some(value) = s.strip_prefix("0x") {
3693 let address = u64::from_str_radix(value, 16).context("invalid BAR address")?;
3694 anyhow::ensure!(address != 0, "BAR address must be nonzero");
3695 vfio_assigned_device_resources::BarAddressConfig::Fixed(address)
3696 } else {
3697 anyhow::bail!("expected 'host' or a hexadecimal address starting with '0x'");
3698 };
3699 Ok(Self(config))
3700 }
3701}
3702
3703#[cfg(target_os = "linux")]
3706#[derive(vmm_cli::KeyValueArgs)]
3707struct BarFlags {
3708 bar0: Option<BarAddressCli>,
3709 bar1: Option<BarAddressCli>,
3710 bar2: Option<BarAddressCli>,
3711 bar3: Option<BarAddressCli>,
3712 bar4: Option<BarAddressCli>,
3713 bar5: Option<BarAddressCli>,
3714}
3715
3716#[cfg(target_os = "linux")]
3718#[derive(vmm_cli::KeyValueArgs)]
3719struct VfioArgs {
3720 host: String,
3721 port: String,
3722 iommu: Option<String>,
3723 #[kv(flatten)]
3724 bars: BarFlags,
3725}
3726
3727#[cfg(target_os = "linux")]
3728impl FromStr for VfioDeviceCli {
3729 type Err = anyhow::Error;
3730
3731 fn from_str(s: &str) -> Result<Self, Self::Err> {
3732 let args: VfioArgs = s.parse()?;
3733
3734 if args.host.contains('/') || args.host.contains("..") {
3736 anyhow::bail!("PCI address must not contain path separators");
3737 }
3738
3739 let bars = args.bars;
3740 let bar_addresses = [
3741 bars.bar0.map(|bar| bar.0).unwrap_or_default(),
3742 bars.bar1.map(|bar| bar.0).unwrap_or_default(),
3743 bars.bar2.map(|bar| bar.0).unwrap_or_default(),
3744 bars.bar3.map(|bar| bar.0).unwrap_or_default(),
3745 bars.bar4.map(|bar| bar.0).unwrap_or_default(),
3746 bars.bar5.map(|bar| bar.0).unwrap_or_default(),
3747 ];
3748
3749 Ok(VfioDeviceCli {
3750 port_name: args.port,
3751 pci_id: args.host,
3752 iommu: args.iommu,
3753 bar_addresses,
3754 })
3755 }
3756}
3757
3758#[cfg(guest_arch = "aarch64")]
3762#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
3763pub struct SmmuCli {
3764 #[kv(key = "rc")]
3766 pub rc_name: String,
3767 #[kv(flag)]
3769 pub accel: bool,
3770 #[kv(default)]
3772 pub oas: SmmuOasCli,
3773}
3774
3775#[cfg(guest_arch = "aarch64")]
3777#[derive(Clone, Copy, Debug, Default)]
3778pub enum SmmuOasCli {
3779 #[default]
3782 Auto,
3783 Fixed(u8),
3785}
3786
3787#[cfg(guest_arch = "aarch64")]
3788impl FromStr for SmmuOasCli {
3789 type Err = anyhow::Error;
3790
3791 fn from_str(s: &str) -> Result<Self, Self::Err> {
3792 Ok(if s == "auto" {
3793 SmmuOasCli::Auto
3794 } else {
3795 SmmuOasCli::Fixed(s.parse().context("oas must be 'auto' or a number")?)
3796 })
3797 }
3798}
3799
3800#[cfg(target_os = "linux")]
3804#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
3805pub struct IommuCli {
3806 pub id: String,
3808}
3809
3810fn default_value_from_arch_env(name: &str) -> OsString {
3818 let prefix = if cfg!(guest_arch = "x86_64") {
3819 "X86_64"
3820 } else if cfg!(guest_arch = "aarch64") {
3821 "AARCH64"
3822 } else {
3823 return Default::default();
3824 };
3825 let prefixed = format!("{}_{}", prefix, name);
3826 std::env::var_os(name)
3827 .or_else(|| std::env::var_os(prefixed))
3828 .unwrap_or_default()
3829}
3830
3831pub fn default_uefi_firmware() -> Option<PathBuf> {
3832 OptionalPathBuf::from(default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE").as_os_str()).0
3833}
3834
3835#[derive(Clone)]
3837pub struct OptionalPathBuf(pub Option<PathBuf>);
3838
3839impl From<&std::ffi::OsStr> for OptionalPathBuf {
3840 fn from(s: &std::ffi::OsStr) -> Self {
3841 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
3842 }
3843}
3844
3845#[cfg(target_os = "linux")]
3846#[derive(Clone)]
3847pub enum VhostUserDeviceTypeCli {
3848 Blk {
3851 num_queues: Option<u16>,
3852 queue_size: Option<u16>,
3853 },
3854 Fs {
3856 tag: String,
3857 num_queues: Option<u16>,
3858 queue_size: Option<u16>,
3859 },
3860 Other {
3862 device_id: u16,
3863 queue_sizes: Vec<u16>,
3864 },
3865}
3866
3867#[cfg(target_os = "linux")]
3868#[derive(Clone)]
3869pub struct VhostUserCli {
3870 pub socket_path: String,
3871 pub device_type: VhostUserDeviceTypeCli,
3872 pub pcie_port: Option<String>,
3873}
3874
3875#[cfg(target_os = "linux")]
3878#[derive(vmm_cli::KeyValueArgs)]
3879struct VhostUserArgs {
3880 #[kv(positional)]
3881 socket_path: String,
3882 #[kv(key = "type")]
3883 type_name: Option<String>,
3884 device_id: Option<u16>,
3885 tag: Option<String>,
3886 pcie_port: Option<String>,
3887 num_queues: Option<u16>,
3888 queue_size: Option<u16>,
3889 queue_sizes: Option<vmm_cli::BracketList<u16>>,
3890}
3891
3892#[cfg(target_os = "linux")]
3893impl FromStr for VhostUserCli {
3894 type Err = anyhow::Error;
3895
3896 fn from_str(s: &str) -> anyhow::Result<Self> {
3897 let mut args: VhostUserArgs = s.parse()?;
3898 let type_name = args.type_name.take();
3899
3900 if type_name.is_some() == args.device_id.is_some() {
3901 anyhow::bail!("must specify type=<name> or device_id=<N>");
3902 }
3903
3904 let device_type = match type_name.as_deref() {
3907 Some("fs") => VhostUserDeviceTypeCli::Fs {
3908 tag: args.tag.take().context("type=fs requires tag=<name>")?,
3909 num_queues: args.num_queues.take(),
3910 queue_size: args.queue_size.take(),
3911 },
3912 Some("blk") => VhostUserDeviceTypeCli::Blk {
3913 num_queues: args.num_queues.take(),
3914 queue_size: args.queue_size.take(),
3915 },
3916 Some(ty) => anyhow::bail!("unknown vhost-user device type: '{ty}'"),
3917 None => {
3918 let queue_sizes = args
3919 .queue_sizes
3920 .take()
3921 .context("device_id= requires queue_sizes=[N,N,...]")?
3922 .0;
3923 anyhow::ensure!(!queue_sizes.is_empty(), "queue_sizes must be non-empty");
3924 VhostUserDeviceTypeCli::Other {
3925 device_id: args.device_id.unwrap(),
3926 queue_sizes,
3927 }
3928 }
3929 };
3930
3931 if args.tag.is_some() {
3932 anyhow::bail!("tag= is only valid for type=fs");
3933 }
3934 if args.queue_sizes.is_some() {
3935 anyhow::bail!("queue_sizes= is only valid for device_id=");
3936 }
3937 if args.num_queues.is_some() || args.queue_size.is_some() {
3938 anyhow::bail!(
3939 "num_queues= and queue_size= are not valid for device_id=; use queue_sizes="
3940 );
3941 }
3942
3943 Ok(VhostUserCli {
3944 socket_path: args.socket_path,
3945 device_type,
3946 pcie_port: args.pcie_port,
3947 })
3948 }
3949}
3950
3951#[cfg(test)]
3952mod tests {
3953 use super::*;
3954
3955 use std::path::Path;
3956 use test_with_tracing::test;
3957
3958 #[test]
3961 fn version_reports_build_info() {
3962 let short = version_output(["openvmm", "-V"]);
3963 assert_eq!(
3964 short,
3965 format!("openvmm {}\n", openvmm_build_info::get().version())
3966 );
3967
3968 let long = version_output(["openvmm", "--version"]);
3969 assert_eq!(
3970 long,
3971 format!("openvmm {}\n", openvmm_build_info::get().long_version())
3972 );
3973 }
3974
3975 fn version_output(args: [&str; 2]) -> String {
3976 let Err(error) = Options::try_parse_from(args) else {
3977 panic!("{args:?} unexpectedly parsed as runtime options");
3978 };
3979 assert_eq!(error.kind(), clap::error::ErrorKind::DisplayVersion);
3980 error.to_string()
3981 }
3982
3983 #[test]
3984 fn test_parse_rpc() {
3985 let rpc = RpcCli::from_str("path=/tmp/openvmm.sock").unwrap();
3987 assert_eq!(rpc.path, Path::new("/tmp/openvmm.sock"));
3988 assert_eq!(rpc.transport, RpcTransportCli::Auto);
3989
3990 for (s, transport) in [
3992 ("auto", RpcTransportCli::Auto),
3993 ("ttrpc", RpcTransportCli::Ttrpc),
3994 ("grpc", RpcTransportCli::Grpc),
3995 ] {
3996 let rpc = RpcCli::from_str(&format!("path=/tmp/s.sock,transport={s}")).unwrap();
3997 assert_eq!(rpc.path, Path::new("/tmp/s.sock"));
3998 assert_eq!(rpc.transport, transport);
3999 }
4000
4001 assert!(RpcCli::from_str("").is_err());
4003 assert!(RpcCli::from_str("transport=ttrpc").is_err());
4004 assert!(RpcCli::from_str("path=").is_err());
4005 assert!(RpcCli::from_str("path=/tmp/s.sock,transport=bogus").is_err());
4006 assert!(RpcCli::from_str("path=/tmp/s.sock,bogus=1").is_err());
4007 assert!(RpcCli::from_str("path=/a,path=/b").is_err());
4008 }
4009
4010 #[test]
4011 fn test_parse_file_opts() {
4012 let disk = DiskCliKind::from_str("file:test.vhd;create=1G").unwrap();
4014 assert!(matches!(
4015 &disk,
4016 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
4017 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
4018 ));
4019
4020 let disk = DiskCliKind::from_str("test.vhd;create=1G").unwrap();
4022 assert!(matches!(
4023 &disk,
4024 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
4025 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
4026 ));
4027
4028 let disk = DiskCliKind::from_str("file:/dev/sdb;direct").unwrap();
4030 assert!(matches!(
4031 &disk,
4032 DiskCliKind::File { path, create_with_len: None, direct: true }
4033 if path == Path::new("/dev/sdb")
4034 ));
4035
4036 let disk = DiskCliKind::from_str("file:disk.img;direct;create=1G").unwrap();
4038 assert!(matches!(
4039 &disk,
4040 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
4041 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
4042 ));
4043
4044 let disk = DiskCliKind::from_str("file:disk.img;create=1G;direct").unwrap();
4045 assert!(matches!(
4046 &disk,
4047 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
4048 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
4049 ));
4050
4051 let disk = DiskCliKind::from_str("file:disk.img").unwrap();
4053 assert!(matches!(
4054 &disk,
4055 DiskCliKind::File { path, create_with_len: None, direct: false }
4056 if path == Path::new("disk.img")
4057 ));
4058
4059 assert!(DiskCliKind::from_str("file:disk.img;bogus").is_err());
4061
4062 assert!(DiskCliKind::from_str("sql:db.sqlite;direct").is_err());
4064 }
4065
4066 #[test]
4067 fn test_parse_memory_disk() {
4068 let s = "mem:1G";
4069 let disk = DiskCliKind::from_str(s).unwrap();
4070 match disk {
4071 DiskCliKind::Memory(size) => {
4072 assert_eq!(size, 1024 * 1024 * 1024); }
4074 _ => panic!("Expected Memory variant"),
4075 }
4076 }
4077
4078 #[test]
4079 fn test_parse_pcie_disk() {
4080 assert_eq!(
4081 DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
4082 Some("p0".to_string())
4083 );
4084 assert_eq!(
4085 DiskCli::from_str("file:path.vhdx,pcie_port=p0")
4086 .unwrap()
4087 .pcie_port,
4088 Some("p0".to_string())
4089 );
4090 assert_eq!(
4091 DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
4092 .unwrap()
4093 .pcie_port,
4094 Some("p0".to_string())
4095 );
4096
4097 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
4099
4100 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
4102 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
4103 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
4104 }
4105
4106 #[test]
4107 fn test_parse_memory_diff_disk() {
4108 let s = "memdiff:file:base.img";
4109 let disk = DiskCliKind::from_str(s).unwrap();
4110 match disk {
4111 DiskCliKind::MemoryDiff(inner) => match *inner {
4112 DiskCliKind::File {
4113 path,
4114 create_with_len,
4115 ..
4116 } => {
4117 assert_eq!(path, PathBuf::from("base.img"));
4118 assert_eq!(create_with_len, None);
4119 }
4120 _ => panic!("Expected File variant inside MemoryDiff"),
4121 },
4122 _ => panic!("Expected MemoryDiff variant"),
4123 }
4124 }
4125
4126 #[test]
4127 fn test_parse_sqlite_disk() {
4128 let s = "sql:db.sqlite;create=2G";
4129 let disk = DiskCliKind::from_str(s).unwrap();
4130 match disk {
4131 DiskCliKind::Sqlite {
4132 path,
4133 create_with_len,
4134 } => {
4135 assert_eq!(path, PathBuf::from("db.sqlite"));
4136 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
4137 }
4138 _ => panic!("Expected Sqlite variant"),
4139 }
4140
4141 let s = "sql:db.sqlite";
4143 let disk = DiskCliKind::from_str(s).unwrap();
4144 match disk {
4145 DiskCliKind::Sqlite {
4146 path,
4147 create_with_len,
4148 } => {
4149 assert_eq!(path, PathBuf::from("db.sqlite"));
4150 assert_eq!(create_with_len, None);
4151 }
4152 _ => panic!("Expected Sqlite variant"),
4153 }
4154 }
4155
4156 #[test]
4157 fn test_parse_sqlite_diff_disk() {
4158 let s = "sqldiff:diff.sqlite;create:file:base.img";
4160 let disk = DiskCliKind::from_str(s).unwrap();
4161 match disk {
4162 DiskCliKind::SqliteDiff { path, create, disk } => {
4163 assert_eq!(path, PathBuf::from("diff.sqlite"));
4164 assert!(create);
4165 match *disk {
4166 DiskCliKind::File {
4167 path,
4168 create_with_len,
4169 ..
4170 } => {
4171 assert_eq!(path, PathBuf::from("base.img"));
4172 assert_eq!(create_with_len, None);
4173 }
4174 _ => panic!("Expected File variant inside SqliteDiff"),
4175 }
4176 }
4177 _ => panic!("Expected SqliteDiff variant"),
4178 }
4179
4180 let s = "sqldiff:diff.sqlite:file:base.img";
4182 let disk = DiskCliKind::from_str(s).unwrap();
4183 match disk {
4184 DiskCliKind::SqliteDiff { path, create, disk } => {
4185 assert_eq!(path, PathBuf::from("diff.sqlite"));
4186 assert!(!create);
4187 match *disk {
4188 DiskCliKind::File {
4189 path,
4190 create_with_len,
4191 ..
4192 } => {
4193 assert_eq!(path, PathBuf::from("base.img"));
4194 assert_eq!(create_with_len, None);
4195 }
4196 _ => panic!("Expected File variant inside SqliteDiff"),
4197 }
4198 }
4199 _ => panic!("Expected SqliteDiff variant"),
4200 }
4201 }
4202
4203 #[test]
4204 fn test_parse_autocache_sqlite_disk() {
4205 let disk =
4207 DiskCliKind::parse_autocache(":file:disk.vhd", Ok("/tmp/cache".to_string())).unwrap();
4208 assert!(matches!(
4209 disk,
4210 DiskCliKind::AutoCacheSqlite {
4211 cache_path,
4212 key,
4213 disk: _disk,
4214 } if cache_path == "/tmp/cache" && key.is_none()
4215 ));
4216
4217 let disk =
4219 DiskCliKind::parse_autocache("mykey:file:disk.vhd", Ok("/tmp/cache".to_string()))
4220 .unwrap();
4221 assert!(matches!(
4222 disk,
4223 DiskCliKind::AutoCacheSqlite {
4224 cache_path,
4225 key: Some(key),
4226 disk: _disk,
4227 } if cache_path == "/tmp/cache" && key == "mykey"
4228 ));
4229
4230 assert!(
4232 DiskCliKind::parse_autocache(":file:disk.vhd", Err(std::env::VarError::NotPresent),)
4233 .is_err()
4234 );
4235 }
4236
4237 #[test]
4238 fn test_parse_disk_errors() {
4239 assert!(DiskCliKind::from_str("invalid:").is_err());
4240 assert!(DiskCliKind::from_str("memory:extra").is_err());
4241
4242 assert!(DiskCliKind::from_str("sqlite:").is_err());
4244 }
4245
4246 #[test]
4247 fn test_parse_errors() {
4248 assert!(DiskCliKind::from_str("mem:invalid").is_err());
4250
4251 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
4253
4254 assert!(
4256 DiskCliKind::parse_autocache("key:file:disk.vhd", Err(std::env::VarError::NotPresent),)
4257 .is_err()
4258 );
4259
4260 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
4262
4263 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
4265
4266 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
4268
4269 assert!(DiskCliKind::from_str("invalid:path").is_err());
4271
4272 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
4274 }
4275
4276 #[test]
4277 fn test_fs_args_from_str() {
4278 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
4279 assert_eq!(args.tag, "tag1");
4280 assert_eq!(args.path, "/path/to/fs");
4281
4282 assert!(FsArgs::from_str("tag1").is_err());
4284 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
4285 }
4286
4287 #[test]
4288 fn test_fs_args_with_options_from_str() {
4289 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
4290 assert_eq!(args.tag, "tag1");
4291 assert_eq!(args.path, "/path/to/fs");
4292 assert_eq!(args.options, "opt1;opt2");
4293
4294 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
4296 assert_eq!(args.tag, "tag1");
4297 assert_eq!(args.path, "/path/to/fs");
4298 assert_eq!(args.options, "");
4299
4300 assert!(FsArgsWithOptions::from_str("tag1").is_err());
4302 }
4303
4304 #[test]
4305 fn test_serial_config_from_str() {
4306 assert_eq!(
4307 SerialConfigCli::from_str("none").unwrap(),
4308 SerialConfigCli::None
4309 );
4310 assert_eq!(
4311 SerialConfigCli::from_str("null").unwrap(),
4312 SerialConfigCli::Null
4313 );
4314 assert_eq!(
4315 SerialConfigCli::from_str("console").unwrap(),
4316 SerialConfigCli::Console
4317 );
4318 assert_eq!(
4319 SerialConfigCli::from_str("stderr").unwrap(),
4320 SerialConfigCli::Stderr
4321 );
4322
4323 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
4325 if let SerialConfigCli::File(path) = file_config {
4326 assert_eq!(path.to_str().unwrap(), "/path/to/file");
4327 } else {
4328 panic!("Expected File variant");
4329 }
4330
4331 match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
4333 SerialConfigCli::NewConsole(None, Some(name)) => {
4334 assert_eq!(name, "MyTerm");
4335 }
4336 _ => panic!("Expected NewConsole variant with name"),
4337 }
4338
4339 match SerialConfigCli::from_str("term").unwrap() {
4341 SerialConfigCli::NewConsole(None, None) => (),
4342 _ => panic!("Expected NewConsole variant without name"),
4343 }
4344
4345 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
4347 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
4348 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
4349 assert_eq!(name, "MyTerm");
4350 }
4351 _ => panic!("Expected NewConsole variant with name"),
4352 }
4353
4354 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
4356 SerialConfigCli::NewConsole(Some(path), None) => {
4357 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
4358 }
4359 _ => panic!("Expected NewConsole variant without name"),
4360 }
4361
4362 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
4364 SerialConfigCli::Tcp(addr) => {
4365 assert_eq!(addr.to_string(), "127.0.0.1:1234");
4366 }
4367 _ => panic!("Expected Tcp variant"),
4368 }
4369
4370 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
4372 SerialConfigCli::Pipe(path) => {
4373 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
4374 }
4375 _ => panic!("Expected Pipe variant"),
4376 }
4377
4378 assert!(SerialConfigCli::from_str("").is_err());
4380 assert!(SerialConfigCli::from_str("unknown").is_err());
4381 assert!(SerialConfigCli::from_str("file").is_err());
4382 assert!(SerialConfigCli::from_str("listen").is_err());
4383 }
4384
4385 #[test]
4386 fn test_endpoint_config_from_str() {
4387 assert!(matches!(
4389 EndpointConfigCli::from_str("none").unwrap(),
4390 EndpointConfigCli::None
4391 ));
4392
4393 match EndpointConfigCli::from_str("consomme").unwrap() {
4395 EndpointConfigCli::Consomme {
4396 cidr: None,
4397 host_fwd,
4398 } => assert!(host_fwd.is_empty()),
4399 _ => panic!("Expected Consomme variant without cidr"),
4400 }
4401
4402 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
4404 EndpointConfigCli::Consomme {
4405 cidr: Some(cidr),
4406 host_fwd,
4407 } => {
4408 assert_eq!(cidr, "192.168.0.0/24");
4409 assert!(host_fwd.is_empty());
4410 }
4411 _ => panic!("Expected Consomme variant with cidr"),
4412 }
4413
4414 match EndpointConfigCli::from_str("consomme:hostfwd=udp:127.0.0.1:5000-:5000").unwrap() {
4416 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4417 assert!(cidr.is_none());
4418 assert_eq!(host_fwd.len(), 1);
4419 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Udp);
4420 assert_eq!(
4421 host_fwd[0].host_address,
4422 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
4423 );
4424 assert_eq!(host_fwd[0].host_port, 5000);
4425 assert_eq!(host_fwd[0].guest_port, 5000);
4426 }
4427 _ => panic!("Expected Consomme variant with hostfwd"),
4428 }
4429
4430 match EndpointConfigCli::from_str("consomme:10.0.0.0/24,hostfwd=tcp::2222-:22").unwrap() {
4432 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4433 assert_eq!(cidr.as_deref(), Some("10.0.0.0/24"));
4434 assert_eq!(host_fwd.len(), 1);
4435 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4436 assert_eq!(host_fwd[0].host_port, 2222);
4437 assert_eq!(host_fwd[0].guest_port, 22);
4438 }
4439 _ => panic!("Expected Consomme variant with cidr and hostfwd"),
4440 }
4441
4442 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::2222-:22,hostfwd=tcp::3389-:3389")
4444 .unwrap()
4445 {
4446 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4447 assert!(cidr.is_none());
4448 assert_eq!(host_fwd.len(), 2);
4449 assert_eq!(host_fwd[0].host_port, 2222);
4450 assert_eq!(host_fwd[0].guest_port, 22);
4451 assert_eq!(host_fwd[1].host_port, 3389);
4452 assert_eq!(host_fwd[1].guest_port, 3389);
4453 }
4454 _ => panic!("Expected Consomme variant with multiple hostfwd"),
4455 }
4456
4457 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:127.0.0.1:8080-:80").unwrap() {
4459 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4460 assert!(cidr.is_none());
4461 assert_eq!(host_fwd.len(), 1);
4462 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4463 assert_eq!(
4464 host_fwd[0].host_address,
4465 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
4466 );
4467 assert_eq!(host_fwd[0].host_port, 8080);
4468 assert_eq!(host_fwd[0].guest_port, 80);
4469 }
4470 _ => panic!("Expected Consomme variant with host/guest port mapping"),
4471 }
4472
4473 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-10.0.0.2:80").unwrap() {
4475 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4476 assert!(cidr.is_none());
4477 assert_eq!(host_fwd[0].host_port, 8080);
4478 assert_eq!(host_fwd[0].guest_port, 80);
4479 }
4480 _ => panic!("Expected Consomme variant with guest address"),
4481 }
4482
4483 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:[::1]:8080-:80").unwrap() {
4485 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4486 assert!(cidr.is_none());
4487 assert_eq!(host_fwd.len(), 1);
4488 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4489 assert_eq!(
4490 host_fwd[0].host_address,
4491 Some(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST))
4492 );
4493 assert_eq!(host_fwd[0].host_port, 8080);
4494 assert_eq!(host_fwd[0].guest_port, 80);
4495 }
4496 _ => panic!("Expected Consomme variant with IPv6 hostfwd"),
4497 }
4498
4499 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-[::1]:80").unwrap() {
4501 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4502 assert!(cidr.is_none());
4503 assert_eq!(host_fwd[0].host_port, 8080);
4504 assert_eq!(host_fwd[0].guest_port, 80);
4505 }
4506 _ => panic!("Expected Consomme variant with IPv6 guest address"),
4507 }
4508
4509 match EndpointConfigCli::from_str("dio").unwrap() {
4511 EndpointConfigCli::Dio { id: None } => (),
4512 _ => panic!("Expected Dio variant without id"),
4513 }
4514
4515 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
4517 EndpointConfigCli::Dio { id: Some(id) } => {
4518 assert_eq!(id, "test_id");
4519 }
4520 _ => panic!("Expected Dio variant with id"),
4521 }
4522
4523 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
4525 EndpointConfigCli::Tap { name } => {
4526 assert_eq!(name, "tap0");
4527 }
4528 _ => panic!("Expected Tap variant"),
4529 }
4530
4531 assert!(EndpointConfigCli::from_str("invalid").is_err());
4533 }
4534
4535 #[test]
4536 fn test_nic_config_from_str() {
4537 use openvmm_defs::config::DeviceVtl;
4538
4539 let config = NicConfigCli::from_str("none").unwrap();
4541 assert_eq!(config.vtl, DeviceVtl::Vtl0);
4542 assert!(config.max_queues.is_none());
4543 assert!(!config.underhill);
4544 assert!(config.pcie_port.is_none());
4545 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4546
4547 let config = NicConfigCli::from_str("vtl2:none").unwrap();
4549 assert_eq!(config.vtl, DeviceVtl::Vtl2);
4550 assert!(config.pcie_port.is_none());
4551 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4552
4553 let config = NicConfigCli::from_str("queues=4:none").unwrap();
4555 assert_eq!(config.max_queues, Some(4));
4556 assert!(config.pcie_port.is_none());
4557 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4558
4559 let config = NicConfigCli::from_str("uh:none").unwrap();
4561 assert!(config.underhill);
4562 assert!(config.pcie_port.is_none());
4563 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4564
4565 let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
4567 assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
4568 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4569
4570 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
4572 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
4574 assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
4575 assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
4576 assert!(NicConfigCli::from_str("pcie_port:none").is_err());
4577 }
4578
4579 #[test]
4580 fn test_parse_pcie_port_prefix() {
4581 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
4583 assert_eq!(port.unwrap(), "rp0");
4584 assert_eq!(rest, "tag,path");
4585
4586 let (port, rest) = parse_pcie_port_prefix("tag,path");
4588 assert!(port.is_none());
4589 assert_eq!(rest, "tag,path");
4590
4591 let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
4593 assert!(port.is_none());
4594 assert_eq!(rest, "pcie_port=:tag,path");
4595
4596 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
4598 assert!(port.is_none());
4599 assert_eq!(rest, "pcie_port=rp0");
4600 }
4601
4602 #[test]
4603 fn test_cxl_test_device_cli_parse_valid() {
4604 let cfg = CxlTestDeviceCli::from_str("mem:1G,pcie_port=rp0").unwrap();
4605 assert_eq!(cfg.hdm_size, 1024 * 1024 * 1024);
4606 assert_eq!(cfg.pcie_port, "rp0");
4607 }
4608
4609 #[test]
4610 fn test_cxl_test_device_cli_parse_invalid() {
4611 assert!(CxlTestDeviceCli::from_str("file:disk.img,pcie_port=rp0").is_err());
4612 assert!(CxlTestDeviceCli::from_str("mem:1G").is_err());
4613 assert!(CxlTestDeviceCli::from_str("mem:1G,pcie_port=").is_err());
4614 }
4615
4616 #[test]
4617 fn test_fs_args_pcie_port() {
4618 let args = FsArgs::from_str("myfs,/path").unwrap();
4620 assert_eq!(args.tag, "myfs");
4621 assert_eq!(args.path, "/path");
4622 assert!(args.pcie_port.is_none());
4623
4624 let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
4626 assert_eq!(args.pcie_port.unwrap(), "rp0");
4627 assert_eq!(args.tag, "myfs");
4628 assert_eq!(args.path, "/path");
4629
4630 assert!(FsArgs::from_str("myfs").is_err());
4632 assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
4633 }
4634
4635 #[test]
4636 fn test_fs_args_with_options_pcie_port() {
4637 let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
4639 assert_eq!(args.tag, "myfs");
4640 assert_eq!(args.path, "/path");
4641 assert_eq!(args.options, "uid=1000");
4642 assert!(args.pcie_port.is_none());
4643
4644 let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
4646 assert_eq!(args.pcie_port.unwrap(), "rp0");
4647 assert_eq!(args.tag, "myfs");
4648 assert_eq!(args.path, "/path");
4649 assert_eq!(args.options, "uid=1000");
4650
4651 assert!(FsArgsWithOptions::from_str("myfs").is_err());
4653 }
4654
4655 #[test]
4656 fn test_virtio_pmem_args_pcie_port() {
4657 let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
4659 assert_eq!(args.path, "/path/to/file");
4660 assert!(args.pcie_port.is_none());
4661
4662 let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
4664 assert_eq!(args.pcie_port.unwrap(), "rp0");
4665 assert_eq!(args.path, "/path/to/file");
4666
4667 assert!(VirtioPmemArgs::from_str("").is_err());
4669 assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
4670 }
4671
4672 #[test]
4673 fn test_smt_config_from_str() {
4674 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
4675 assert_eq!(
4676 SmtConfigCli::from_str("force").unwrap(),
4677 SmtConfigCli::Force
4678 );
4679 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
4680
4681 assert!(SmtConfigCli::from_str("invalid").is_err());
4683 assert!(SmtConfigCli::from_str("").is_err());
4684 }
4685
4686 #[test]
4687 fn test_pcat_boot_order_from_str() {
4688 let order = PcatBootOrderCli::from_str("optical").unwrap();
4690 assert_eq!(order.0[0], PcatBootDevice::Optical);
4691
4692 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
4694 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
4695 assert_eq!(order.0[1], PcatBootDevice::Network);
4696
4697 assert!(PcatBootOrderCli::from_str("invalid").is_err());
4699 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
4701
4702 #[test]
4703 fn test_floppy_disk_from_str() {
4704 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
4706 assert!(!disk.read_only);
4707 match disk.kind {
4708 DiskCliKind::File {
4709 path,
4710 create_with_len,
4711 ..
4712 } => {
4713 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
4714 assert_eq!(create_with_len, None);
4715 }
4716 _ => panic!("Expected File variant"),
4717 }
4718
4719 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
4721 assert!(disk.read_only);
4722
4723 assert!(FloppyDiskCli::from_str("").is_err());
4725 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
4726 }
4727
4728 #[test]
4729 fn test_pcie_root_complex_from_str() {
4730 const ONE_MB: u64 = 1024 * 1024;
4731 const ONE_GB: u64 = 1024 * ONE_MB;
4732
4733 const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
4734 const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
4735 const DEFAULT_HDM: u64 = ONE_GB;
4736 const DEFAULT_HDM_WINDOW_RESTRICTIONS: CfmwsWindowRestrictions =
4737 CfmwsWindowRestrictions::DEVICE_COHERENT;
4738
4739 assert_eq!(
4740 PcieRootComplexCli::from_str("rc0").unwrap(),
4741 PcieRootComplexCli {
4742 name: "rc0".to_string(),
4743 segment: 0,
4744 start_bus: 0,
4745 end_bus: 255,
4746 low_mmio: DEFAULT_LOW_MMIO,
4747 high_mmio: DEFAULT_HIGH_MMIO,
4748 hdm: DEFAULT_HDM,
4749 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4750 vnode: None,
4751 low_mmio_base: None,
4752 high_mmio_base: None,
4753 preserve_bars: false,
4754 }
4755 );
4756
4757 assert_eq!(
4758 PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
4759 PcieRootComplexCli {
4760 name: "rc1".to_string(),
4761 segment: 1,
4762 start_bus: 0,
4763 end_bus: 255,
4764 low_mmio: DEFAULT_LOW_MMIO,
4765 high_mmio: DEFAULT_HIGH_MMIO,
4766 hdm: DEFAULT_HDM,
4767 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4768 vnode: None,
4769 low_mmio_base: None,
4770 high_mmio_base: None,
4771 preserve_bars: false,
4772 }
4773 );
4774
4775 assert_eq!(
4776 PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
4777 PcieRootComplexCli {
4778 name: "rc2".to_string(),
4779 segment: 0,
4780 start_bus: 32,
4781 end_bus: 255,
4782 low_mmio: DEFAULT_LOW_MMIO,
4783 high_mmio: DEFAULT_HIGH_MMIO,
4784 hdm: DEFAULT_HDM,
4785 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4786 vnode: None,
4787 low_mmio_base: None,
4788 high_mmio_base: None,
4789 preserve_bars: false,
4790 }
4791 );
4792
4793 assert_eq!(
4794 PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
4795 PcieRootComplexCli {
4796 name: "rc3".to_string(),
4797 segment: 0,
4798 start_bus: 0,
4799 end_bus: 31,
4800 low_mmio: DEFAULT_LOW_MMIO,
4801 high_mmio: DEFAULT_HIGH_MMIO,
4802 hdm: DEFAULT_HDM,
4803 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4804 vnode: None,
4805 low_mmio_base: None,
4806 high_mmio_base: None,
4807 preserve_bars: false,
4808 }
4809 );
4810
4811 assert_eq!(
4812 PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
4813 PcieRootComplexCli {
4814 name: "rc4".to_string(),
4815 segment: 0,
4816 start_bus: 32,
4817 end_bus: 127,
4818 low_mmio: DEFAULT_LOW_MMIO,
4819 high_mmio: 2 * ONE_GB,
4820 hdm: DEFAULT_HDM,
4821 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4822 vnode: None,
4823 low_mmio_base: None,
4824 high_mmio_base: None,
4825 preserve_bars: false,
4826 }
4827 );
4828
4829 assert_eq!(
4830 PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
4831 PcieRootComplexCli {
4832 name: "rc5".to_string(),
4833 segment: 2,
4834 start_bus: 32,
4835 end_bus: 127,
4836 low_mmio: DEFAULT_LOW_MMIO,
4837 high_mmio: DEFAULT_HIGH_MMIO,
4838 hdm: DEFAULT_HDM,
4839 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4840 vnode: None,
4841 low_mmio_base: None,
4842 high_mmio_base: None,
4843 preserve_bars: false,
4844 }
4845 );
4846
4847 assert_eq!(
4848 PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
4849 PcieRootComplexCli {
4850 name: "rc6".to_string(),
4851 segment: 0,
4852 start_bus: 0,
4853 end_bus: 255,
4854 low_mmio: ONE_MB as u32,
4855 high_mmio: 64 * ONE_GB,
4856 hdm: DEFAULT_HDM,
4857 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4858 vnode: None,
4859 low_mmio_base: None,
4860 high_mmio_base: None,
4861 preserve_bars: false,
4862 }
4863 );
4864
4865 assert_eq!(
4866 PcieRootComplexCli::from_str("rc7,hdm=2G").unwrap(),
4867 PcieRootComplexCli {
4868 name: "rc7".to_string(),
4869 segment: 0,
4870 start_bus: 0,
4871 end_bus: 255,
4872 low_mmio: DEFAULT_LOW_MMIO,
4873 high_mmio: DEFAULT_HIGH_MMIO,
4874 hdm: 2 * ONE_GB,
4875 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4876 vnode: None,
4877 low_mmio_base: None,
4878 high_mmio_base: None,
4879 preserve_bars: false,
4880 }
4881 );
4882
4883 assert_eq!(
4884 PcieRootComplexCli::from_str("rc8,hdm_window_restrictions=0x21").unwrap(),
4885 PcieRootComplexCli {
4886 name: "rc8".to_string(),
4887 segment: 0,
4888 start_bus: 0,
4889 end_bus: 255,
4890 low_mmio: DEFAULT_LOW_MMIO,
4891 high_mmio: DEFAULT_HIGH_MMIO,
4892 hdm: DEFAULT_HDM,
4893 hdm_window_restrictions: CfmwsWindowRestrictions::try_from_bits(0x21).unwrap(),
4894 vnode: None,
4895 low_mmio_base: None,
4896 high_mmio_base: None,
4897 preserve_bars: false,
4898 }
4899 );
4900
4901 assert!(PcieRootComplexCli::from_str("").is_err());
4903 assert!(PcieRootComplexCli::from_str("poorly,").is_err());
4904 assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
4905 assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
4906 assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
4907 assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
4908 assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
4909 assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
4910 assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
4911 assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
4912 assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
4913 assert!(PcieRootComplexCli::from_str("rc,hdm=bad").is_err());
4914 assert!(PcieRootComplexCli::from_str("rc,hdm").is_err());
4915 assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions=bad").is_err());
4916 assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions").is_err());
4917 assert!(PcieRootComplexCli::from_str("rc,cxl").is_err());
4918
4919 assert_eq!(
4921 PcieRootComplexCli::from_str("rc9,node=1").unwrap(),
4922 PcieRootComplexCli {
4923 name: "rc9".to_string(),
4924 segment: 0,
4925 start_bus: 0,
4926 end_bus: 255,
4927 low_mmio: DEFAULT_LOW_MMIO,
4928 high_mmio: DEFAULT_HIGH_MMIO,
4929 hdm: DEFAULT_HDM,
4930 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4931 vnode: Some(1),
4932 low_mmio_base: None,
4933 high_mmio_base: None,
4934 preserve_bars: false,
4935 }
4936 );
4937 }
4938
4939 #[test]
4940 fn test_pcie_root_port_from_str() {
4941 assert_eq!(
4942 PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
4943 PcieRootPortCli {
4944 root_complex_name: "rc0".to_string(),
4945 name: "rc0rp0".to_string(),
4946 devfn: None,
4947 hotplug: false,
4948 acs_capabilities_supported: None,
4949 cxl: false,
4950 pasid: false,
4951 }
4952 );
4953
4954 assert_eq!(
4955 PcieRootPortCli::from_str("my_rc:port2").unwrap(),
4956 PcieRootPortCli {
4957 root_complex_name: "my_rc".to_string(),
4958 name: "port2".to_string(),
4959 devfn: None,
4960 hotplug: false,
4961 acs_capabilities_supported: None,
4962 cxl: false,
4963 pasid: false,
4964 }
4965 );
4966
4967 assert_eq!(
4969 PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
4970 PcieRootPortCli {
4971 root_complex_name: "my_rc".to_string(),
4972 name: "port2".to_string(),
4973 devfn: None,
4974 hotplug: true,
4975 acs_capabilities_supported: None,
4976 cxl: false,
4977 pasid: false,
4978 }
4979 );
4980
4981 assert_eq!(
4982 PcieRootPortCli::from_str("my_rc:port3,acs=0").unwrap(),
4983 PcieRootPortCli {
4984 root_complex_name: "my_rc".to_string(),
4985 name: "port3".to_string(),
4986 devfn: None,
4987 hotplug: false,
4988 acs_capabilities_supported: Some(0),
4989 cxl: false,
4990 pasid: false,
4991 }
4992 );
4993
4994 assert_eq!(
4995 PcieRootPortCli::from_str("my_rc:port3,acs=0x5f").unwrap(),
4996 PcieRootPortCli {
4997 root_complex_name: "my_rc".to_string(),
4998 name: "port3".to_string(),
4999 devfn: None,
5000 hotplug: false,
5001 acs_capabilities_supported: Some(0x005f),
5002 cxl: false,
5003 pasid: false,
5004 }
5005 );
5006
5007 assert_eq!(
5008 PcieRootPortCli::from_str("my_rc:port4,cxl").unwrap(),
5009 PcieRootPortCli {
5010 root_complex_name: "my_rc".to_string(),
5011 name: "port4".to_string(),
5012 devfn: None,
5013 hotplug: false,
5014 acs_capabilities_supported: None,
5015 cxl: true,
5016 pasid: false,
5017 }
5018 );
5019
5020 assert_eq!(
5022 PcieRootPortCli::from_str("my_rc:port5,addr=5").unwrap(),
5023 PcieRootPortCli {
5024 root_complex_name: "my_rc".to_string(),
5025 name: "port5".to_string(),
5026 devfn: Some(5 << 3),
5027 hotplug: false,
5028 acs_capabilities_supported: None,
5029 cxl: false,
5030 pasid: false,
5031 }
5032 );
5033 assert_eq!(
5034 PcieRootPortCli::from_str("my_rc:port6,addr=5.1").unwrap(),
5035 PcieRootPortCli {
5036 root_complex_name: "my_rc".to_string(),
5037 name: "port6".to_string(),
5038 devfn: Some((5 << 3) | 1),
5039 hotplug: false,
5040 acs_capabilities_supported: None,
5041 cxl: false,
5042 pasid: false,
5043 }
5044 );
5045 assert_eq!(
5046 PcieRootPortCli::from_str("my_rc:port7,addr=0x1f.7").unwrap(),
5047 PcieRootPortCli {
5048 root_complex_name: "my_rc".to_string(),
5049 name: "port7".to_string(),
5050 devfn: Some(0xff),
5051 hotplug: false,
5052 acs_capabilities_supported: None,
5053 cxl: false,
5054 pasid: false,
5055 }
5056 );
5057
5058 assert_eq!(
5059 PcieRootPortCli::from_str("my_rc:port8,pasid").unwrap(),
5060 PcieRootPortCli {
5061 root_complex_name: "my_rc".to_string(),
5062 name: "port8".to_string(),
5063 devfn: None,
5064 hotplug: false,
5065 acs_capabilities_supported: None,
5066 cxl: false,
5067 pasid: true,
5068 }
5069 );
5070
5071 assert!(PcieRootPortCli::from_str("").is_err());
5073 assert!(PcieRootPortCli::from_str("rp0").is_err());
5074 assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
5075 assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
5076 assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
5077 assert!(PcieRootPortCli::from_str("rc0:rp0,cxl=true").is_err());
5078 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=32").is_err());
5079 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=0.8").is_err());
5080 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=1.2.3").is_err());
5081 assert!(PcieRootPortCli::from_str("rc0:rp0,addr").is_err());
5082 assert!(PcieRootPortCli::from_str("rc0:rp0,pasid=foo").is_err());
5083 }
5084
5085 #[test]
5086 fn test_pcie_generic_initiator_from_str() {
5087 assert_eq!(
5088 PcieGenericInitiatorCli::from_str("port=rp0,node=1").unwrap(),
5089 PcieGenericInitiatorCli {
5090 port_name: "rp0".to_string(),
5091 node: 1,
5092 }
5093 );
5094
5095 assert_eq!(
5097 PcieGenericInitiatorCli::from_str("node=2,port=sw0-downstream-1").unwrap(),
5098 PcieGenericInitiatorCli {
5099 port_name: "sw0-downstream-1".to_string(),
5100 node: 2,
5101 }
5102 );
5103
5104 assert!(PcieGenericInitiatorCli::from_str("").is_err());
5106 assert!(PcieGenericInitiatorCli::from_str("port=rp0").is_err());
5107 assert!(PcieGenericInitiatorCli::from_str("node=1").is_err());
5108 assert!(PcieGenericInitiatorCli::from_str("rp0=1").is_err());
5109 assert!(PcieGenericInitiatorCli::from_str("port=,node=1").is_err());
5110 assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=x").is_err());
5111 assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=1,extra").is_err());
5112 }
5113
5114 #[test]
5115 fn test_pcie_switch_from_str() {
5116 assert_eq!(
5117 GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
5118 GenericPcieSwitchCli {
5119 port_name: "rp0".to_string(),
5120 name: "switch0".to_string(),
5121 num_downstream_ports: 4,
5122 hotplug: false,
5123 acs_capabilities_supported: None,
5124 pasid: false,
5125 }
5126 );
5127
5128 assert_eq!(
5129 GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
5130 GenericPcieSwitchCli {
5131 port_name: "port1".to_string(),
5132 name: "my_switch".to_string(),
5133 num_downstream_ports: 4,
5134 hotplug: false,
5135 acs_capabilities_supported: None,
5136 pasid: false,
5137 }
5138 );
5139
5140 assert_eq!(
5141 GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
5142 GenericPcieSwitchCli {
5143 port_name: "rp2".to_string(),
5144 name: "sw".to_string(),
5145 num_downstream_ports: 8,
5146 hotplug: false,
5147 acs_capabilities_supported: None,
5148 pasid: false,
5149 }
5150 );
5151
5152 assert_eq!(
5154 GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
5155 GenericPcieSwitchCli {
5156 port_name: "switch0-downstream-1".to_string(),
5157 name: "child_switch".to_string(),
5158 num_downstream_ports: 4,
5159 hotplug: false,
5160 acs_capabilities_supported: None,
5161 pasid: false,
5162 }
5163 );
5164
5165 assert_eq!(
5167 GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
5168 GenericPcieSwitchCli {
5169 port_name: "rp0".to_string(),
5170 name: "switch0".to_string(),
5171 num_downstream_ports: 4,
5172 hotplug: true,
5173 acs_capabilities_supported: None,
5174 pasid: false,
5175 }
5176 );
5177
5178 assert_eq!(
5180 GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
5181 GenericPcieSwitchCli {
5182 port_name: "rp0".to_string(),
5183 name: "switch0".to_string(),
5184 num_downstream_ports: 8,
5185 hotplug: true,
5186 acs_capabilities_supported: None,
5187 pasid: false,
5188 }
5189 );
5190
5191 assert_eq!(
5192 GenericPcieSwitchCli::from_str("rp0:switch0,acs=0").unwrap(),
5193 GenericPcieSwitchCli {
5194 port_name: "rp0".to_string(),
5195 name: "switch0".to_string(),
5196 num_downstream_ports: 4,
5197 hotplug: false,
5198 acs_capabilities_supported: Some(0),
5199 pasid: false,
5200 }
5201 );
5202
5203 assert_eq!(
5204 GenericPcieSwitchCli::from_str("rp0:switch0,acs=95").unwrap(),
5205 GenericPcieSwitchCli {
5206 port_name: "rp0".to_string(),
5207 name: "switch0".to_string(),
5208 num_downstream_ports: 4,
5209 hotplug: false,
5210 acs_capabilities_supported: Some(95),
5211 pasid: false,
5212 }
5213 );
5214
5215 assert_eq!(
5216 GenericPcieSwitchCli::from_str("rp0:switch0,pasid").unwrap(),
5217 GenericPcieSwitchCli {
5218 port_name: "rp0".to_string(),
5219 name: "switch0".to_string(),
5220 num_downstream_ports: 4,
5221 hotplug: false,
5222 acs_capabilities_supported: None,
5223 pasid: true,
5224 }
5225 );
5226
5227 assert!(GenericPcieSwitchCli::from_str("").is_err());
5229 assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
5230 assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
5231 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
5232 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
5233 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
5234 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
5235 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,pasid=bar").is_err());
5236 }
5237
5238 #[test]
5239 fn test_pcie_remote_from_str() {
5240 assert_eq!(
5242 PcieRemoteCli::from_str("rc0rp0").unwrap(),
5243 PcieRemoteCli {
5244 port_name: "rc0rp0".to_string(),
5245 socket_addr: None,
5246 hu: 0,
5247 controller: 0,
5248 }
5249 );
5250
5251 assert_eq!(
5253 PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
5254 PcieRemoteCli {
5255 port_name: "rc0rp0".to_string(),
5256 socket_addr: Some("localhost:22567".to_string()),
5257 hu: 0,
5258 controller: 0,
5259 }
5260 );
5261
5262 assert_eq!(
5264 PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
5265 PcieRemoteCli {
5266 port_name: "myport".to_string(),
5267 socket_addr: Some("localhost:22568".to_string()),
5268 hu: 1,
5269 controller: 2,
5270 }
5271 );
5272
5273 assert_eq!(
5275 PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
5276 PcieRemoteCli {
5277 port_name: "port0".to_string(),
5278 socket_addr: None,
5279 hu: 5,
5280 controller: 3,
5281 }
5282 );
5283
5284 assert!(PcieRemoteCli::from_str("").is_err());
5286 assert!(PcieRemoteCli::from_str("port,socket=").is_err());
5287 assert!(PcieRemoteCli::from_str("port,hu=").is_err());
5288 assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
5289 assert!(PcieRemoteCli::from_str("port,controller=").is_err());
5290 assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
5291 assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
5292 }
5293
5294 #[test]
5295 fn test_parse_memory_units() {
5296 assert_eq!(parse_memory("64G").unwrap(), 64 * 1024 * 1024 * 1024);
5297 assert_eq!(parse_memory("64GB").unwrap(), 64 * 1024 * 1024 * 1024);
5298 assert_eq!(parse_memory("3MB").unwrap(), 3 * 1024 * 1024);
5299 assert_eq!(parse_memory("512KB").unwrap(), 512 * 1024);
5300 assert!(parse_memory("3MiB").is_err());
5301 }
5302
5303 #[test]
5304 fn test_memory_config_size_only() {
5305 assert_eq!(
5306 parse_memory_config("64G").unwrap(),
5307 MemoryCli {
5308 size: Some(vmm_cli::MemorySize(64 * 1024 * 1024 * 1024)),
5309 ..Default::default()
5310 }
5311 );
5312 }
5313
5314 #[test]
5315 fn test_memory_config_key_value() {
5316 assert_eq!(
5317 parse_memory_config("size=2G,shared=off,prefetch=on,thp=on").unwrap(),
5318 MemoryCli {
5319 size: Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024)),
5320 shared: Some(false),
5321 prefetch: true,
5322 transparent_hugepages: Some(true),
5323 ..Default::default()
5324 }
5325 );
5326
5327 assert_eq!(
5328 parse_memory_config("size=4GB,hugepages=on,hugepage_size=2MB").unwrap(),
5329 MemoryCli {
5330 size: Some(vmm_cli::MemorySize(4 * 1024 * 1024 * 1024)),
5331 hugepages: true,
5332 hugepage_size: Some(vmm_cli::MemorySize(2 * 1024 * 1024)),
5333 ..Default::default()
5334 }
5335 );
5336
5337 assert_eq!(
5338 parse_memory_config("file=/tmp/memory.bin").unwrap(),
5339 MemoryCli {
5340 file: Some(PathBuf::from("/tmp/memory.bin")),
5341 ..Default::default()
5342 }
5343 );
5344 }
5345
5346 #[test]
5347 fn test_memory_config_rejects_invalid_combinations() {
5348 assert!(parse_memory_config("size=1G,size=2G").is_err());
5349 assert!(parse_memory_config("hugepage_size=2M").is_err());
5350 assert!(parse_memory_config("hugepages=on,shared=off").is_err());
5351 assert!(parse_memory_config("hugepages=on,file=/tmp/memory.bin").is_err());
5352
5353 assert_eq!(
5356 parse_memory_config("hugepages=on,hugepage_size=3MB")
5357 .unwrap()
5358 .hugepage_size,
5359 Some(vmm_cli::MemorySize(3 * 1024 * 1024))
5360 );
5361 }
5362
5363 #[test]
5364 fn test_smbios_requires_type() {
5365 assert!(parse_smbios("manufacturer=Contoso,family=Foo").is_err());
5367 }
5368
5369 #[test]
5370 fn test_smbios_all_system_keys() {
5371 let parsed = parse_smbios(
5372 "type=1,manufacturer=M,product=P,version=V,serial=S,sku=K,family=F,\
5373 uuid=12345678-9abc-def0-1234-56789abcdef0",
5374 )
5375 .unwrap();
5376 assert_eq!(
5377 parsed,
5378 SmbiosCli {
5379 system: SmbiosSystemCli {
5380 manufacturer: Some("M".to_string()),
5381 product_name: Some("P".to_string()),
5382 version: Some("V".to_string()),
5383 serial_number: Some("S".to_string()),
5384 sku_number: Some("K".to_string()),
5385 family: Some("F".to_string()),
5386 uuid: Some(SmbiosUuid::Fixed(
5387 "12345678-9abc-def0-1234-56789abcdef0"
5388 .parse::<Guid>()
5389 .unwrap()
5390 )),
5391 },
5392 ..Default::default()
5393 }
5394 );
5395 }
5396
5397 #[test]
5398 fn test_smbios_bios_keys() {
5399 let parsed =
5400 parse_smbios("type=0,vendor=Contoso,version=1.0,date=01/01/2026,release=4.1").unwrap();
5401 assert_eq!(
5402 parsed,
5403 SmbiosCli {
5404 bios: SmbiosBiosCli {
5405 vendor: Some("Contoso".to_string()),
5406 version: Some("1.0".to_string()),
5407 release_date: Some("01/01/2026".to_string()),
5408 release: Some(SmbiosRelease(4, 1)),
5409 },
5410 ..Default::default()
5411 }
5412 );
5413 }
5414
5415 #[test]
5416 fn test_smbios_release_parsing() {
5417 assert_eq!(
5419 parse_smbios("type=0,release=4.1").unwrap().bios.release,
5420 Some(SmbiosRelease(4, 1))
5421 );
5422 assert_eq!(
5423 parse_smbios("type=0,release=255.0").unwrap().bios.release,
5424 Some(SmbiosRelease(255, 0))
5425 );
5426 assert!(parse_smbios("type=0,release=4").is_err());
5429 assert!(parse_smbios("type=0,release=4.1.0").is_err());
5430 assert!(parse_smbios("type=0,release=256.0").is_err());
5431 assert!(parse_smbios("type=0,release=x.y").is_err());
5432 assert!(parse_smbios("type=1,release=4.1").is_err());
5434 }
5435
5436 #[test]
5437 fn test_smbios_uuid() {
5438 assert_eq!(
5440 parse_smbios("type=1,uuid=12345678-9abc-def0-1234-56789abcdef0")
5441 .unwrap()
5442 .system
5443 .uuid,
5444 Some(SmbiosUuid::Fixed(
5445 "12345678-9abc-def0-1234-56789abcdef0"
5446 .parse::<Guid>()
5447 .unwrap()
5448 ))
5449 );
5450 assert_eq!(
5452 parse_smbios("type=1,uuid=random").unwrap().system.uuid,
5453 Some(SmbiosUuid::Random)
5454 );
5455 }
5456
5457 #[test]
5458 fn test_smbios_version_disambiguated_by_type() {
5459 let bios = parse_smbios("type=0,version=1.0").unwrap();
5461 assert_eq!(bios.bios.version.as_deref(), Some("1.0"));
5462 assert_eq!(bios.system.version, None);
5463
5464 let system = parse_smbios("type=1,version=2.0").unwrap();
5465 assert_eq!(system.system.version.as_deref(), Some("2.0"));
5466 assert_eq!(system.bios.version, None);
5467 }
5468
5469 #[test]
5470 fn test_smbios_rejects_invalid() {
5471 assert!(parse_smbios("manufacturer=M").is_err());
5473 assert!(parse_smbios("type=0,manufacturer=M").is_err());
5475 assert!(parse_smbios("type=1,nonsense=x").is_err());
5476 assert!(parse_smbios("type=2,vendor=M").is_err());
5478 assert!(parse_smbios("type=bad,vendor=M").is_err());
5479 assert!(parse_smbios("type=1,manufacturer").is_err());
5481 assert!(parse_smbios("=value").is_err());
5482 assert!(parse_smbios("type=1,manufacturer=").is_err());
5483 assert!(parse_smbios("type=1,manufacturer=A,manufacturer=B").is_err());
5485 assert!(parse_smbios("type=0,type=1,vendor=M").is_err());
5486 assert!(parse_smbios("type=1,uuid=not-a-guid").is_err());
5488 }
5489
5490 #[test]
5491 fn test_smbios_merge_across_arguments() {
5492 let mut merged = SmbiosCli::default();
5493 merged
5494 .merge(parse_smbios("type=0,vendor=Contoso").unwrap())
5495 .unwrap();
5496 merged
5497 .merge(parse_smbios("type=1,manufacturer=M,family=F").unwrap())
5498 .unwrap();
5499 assert_eq!(
5500 merged,
5501 SmbiosCli {
5502 bios: SmbiosBiosCli {
5503 vendor: Some("Contoso".to_string()),
5504 ..Default::default()
5505 },
5506 system: SmbiosSystemCli {
5507 manufacturer: Some("M".to_string()),
5508 family: Some("F".to_string()),
5509 ..Default::default()
5510 },
5511 }
5512 );
5513 }
5514
5515 #[test]
5516 fn test_smbios_merge_rejects_conflicts() {
5517 let mut merged = SmbiosCli::default();
5518 merged
5519 .merge(parse_smbios("type=1,manufacturer=A").unwrap())
5520 .unwrap();
5521 assert!(
5523 merged
5524 .merge(parse_smbios("type=1,manufacturer=B").unwrap())
5525 .is_err()
5526 );
5527 }
5528
5529 #[test]
5530 fn test_smbios_arg_repeatable() {
5531 let opt = Options::try_parse_from([
5532 "openvmm",
5533 "--smbios",
5534 "type=1,manufacturer=Contoso",
5535 "--smbios",
5536 "type=0,vendor=Acme",
5537 ])
5538 .unwrap();
5539 assert_eq!(opt.smbios.len(), 2);
5540 assert_eq!(
5541 opt.smbios[0].system.manufacturer.as_deref(),
5542 Some("Contoso")
5543 );
5544 assert_eq!(opt.smbios[1].bios.vendor.as_deref(), Some("Acme"));
5545 }
5546
5547 #[test]
5548 fn test_memory_options_merge_legacy_aliases() {
5549 let opt = Options::try_parse_from([
5550 "openvmm",
5551 "--memory",
5552 "2G",
5553 "--prefetch",
5554 "--private-memory",
5555 "--thp",
5556 ])
5557 .unwrap();
5558 opt.validate_memory_options().unwrap();
5559 assert_eq!(opt.memory_size(), 2 * 1024 * 1024 * 1024);
5560 assert!(opt.prefetch_memory());
5561 assert!(opt.private_memory());
5562 assert!(opt.transparent_hugepages());
5563 }
5564
5565 #[test]
5566 fn test_serial_debugger_mode_option_parsed() {
5567 let opt = Options::try_parse_from(["openvmm"]).unwrap();
5569 assert!(opt.com1.is_none());
5570
5571 let opt = Options::try_parse_from(["openvmm", "--com1", "none"]).unwrap();
5573 let com1 = opt.com1.unwrap();
5574 assert!(!com1.debugger_mode);
5575 assert_eq!(com1.backend, SerialConfigCli::None);
5576
5577 let opt = Options::try_parse_from([
5580 "openvmm",
5581 "--com1",
5582 "debugger-mode:listen=/tmp/kd",
5583 "--com2",
5584 "none",
5585 ])
5586 .unwrap();
5587 let com1 = opt.com1.unwrap();
5588 assert!(com1.debugger_mode);
5589 assert_eq!(com1.backend, SerialConfigCli::Pipe("/tmp/kd".into()));
5590 assert!(!opt.com2.unwrap().debugger_mode);
5592
5593 let opt = Options::try_parse_from([
5595 "openvmm",
5596 "--com1",
5597 "debugger-mode:listen=tcp:127.0.0.1:5555",
5598 ])
5599 .unwrap();
5600 let com1 = opt.com1.unwrap();
5601 assert!(com1.debugger_mode);
5602 assert_eq!(
5603 com1.backend,
5604 SerialConfigCli::Tcp("127.0.0.1:5555".parse().unwrap())
5605 );
5606 }
5607
5608 #[test]
5609 fn test_no_hv_requires_no_vmbus() {
5610 assert!(Options::try_parse_from(["openvmm", "--no-hv"]).is_err());
5611
5612 let opt = Options::try_parse_from(["openvmm", "--uefi", "--no-hv", "--no-vmbus"]).unwrap();
5613 assert!(opt.no_hv);
5614 assert!(opt.no_vmbus);
5615
5616 assert!(
5617 Options::try_parse_from(["openvmm", "--uefi", "--no-hv", "--no-vmbus", "--hv"])
5618 .is_err()
5619 );
5620 }
5621
5622 #[test]
5623 fn test_memory_options_allow_legacy_thp_with_new_private_memory() {
5624 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=off", "--thp"]).unwrap();
5625 opt.validate_memory_options().unwrap();
5626 assert!(opt.private_memory());
5627 assert!(opt.transparent_hugepages());
5628 }
5629
5630 #[test]
5631 fn test_memory_options_reject_conflicting_legacy_aliases() {
5632 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=on", "--private-memory"])
5633 .unwrap();
5634 assert!(opt.validate_memory_options().is_err());
5635 }
5636
5637 #[test]
5638 fn test_isolation_options_reject_snp_uefi() {
5639 let opt = Options::try_parse_from(["openvmm", "--isolation", "snp", "--uefi"]).unwrap();
5640
5641 assert_eq!(
5642 opt.validate_isolation_options().unwrap_err().to_string(),
5643 "SNP isolation currently only supports Linux direct boot"
5644 );
5645 }
5646
5647 #[test]
5648 fn test_isolation_options_reject_snp_hugepages() {
5649 for args in [
5650 vec![
5651 "openvmm",
5652 "--isolation",
5653 "snp",
5654 "--memory",
5655 "size=1G,hugepages=on",
5656 ],
5657 vec![
5658 "openvmm",
5659 "--isolation",
5660 "snp",
5661 "--numa",
5662 "size=1G,hugepages=on",
5663 ],
5664 ] {
5665 let opt = Options::try_parse_from(args).unwrap();
5666 assert_eq!(
5667 opt.validate_isolation_options().unwrap_err().to_string(),
5668 "SNP isolation currently does not support hugetlb memory"
5669 );
5670 }
5671 }
5672
5673 #[test]
5674 fn test_restricted_injection_requires_snp() {
5675 let opt = Options::try_parse_from(["openvmm", "--snp-restricted-injection"]).unwrap();
5676
5677 assert_eq!(
5678 opt.validate_isolation_options().unwrap_err().to_string(),
5679 "--snp-restricted-injection requires --isolation snp"
5680 );
5681
5682 let opt = Options::try_parse_from([
5683 "openvmm",
5684 "--isolation",
5685 "snp",
5686 "--snp-restricted-injection",
5687 ])
5688 .unwrap();
5689 assert_eq!(
5690 opt.validate_isolation_options().unwrap_err().to_string(),
5691 "--snp-restricted-injection requires --hypervisor mshv"
5692 );
5693
5694 let opt = Options::try_parse_from([
5695 "openvmm",
5696 "--hypervisor",
5697 "mshv",
5698 "--isolation",
5699 "snp",
5700 "--snp-restricted-injection",
5701 ])
5702 .unwrap();
5703 opt.validate_isolation_options().unwrap();
5704
5705 let opt = Options::try_parse_from([
5706 "openvmm",
5707 "--hypervisor",
5708 "mshv",
5709 "--isolation",
5710 "snp",
5711 "--snp-restricted-injection",
5712 "--pcat",
5713 ])
5714 .unwrap();
5715 assert_eq!(
5716 opt.validate_isolation_options().unwrap_err().to_string(),
5717 "--snp-restricted-injection requires Linux direct boot"
5718 );
5719
5720 let opt = Options::try_parse_from([
5721 "openvmm",
5722 "--hypervisor",
5723 "mshv",
5724 "--isolation",
5725 "snp",
5726 "--snp-restricted-injection",
5727 "--restore-snapshot",
5728 "snapshot",
5729 ])
5730 .unwrap();
5731 assert_eq!(
5732 opt.validate_isolation_options().unwrap_err().to_string(),
5733 "--snp-restricted-injection requires Linux direct boot"
5734 );
5735 }
5736
5737 #[test]
5738 fn test_isolation_options_allow_vbs_uefi() {
5739 let opt = Options::try_parse_from(["openvmm", "--isolation", "vbs", "--uefi"]).unwrap();
5740
5741 opt.validate_isolation_options().unwrap();
5742 }
5743
5744 #[test]
5745 fn test_isolation_options_allow_vbs_hugepages() {
5746 let opt = Options::try_parse_from([
5747 "openvmm",
5748 "--isolation",
5749 "vbs",
5750 "--memory",
5751 "size=1G,hugepages=on",
5752 ])
5753 .unwrap();
5754
5755 opt.validate_isolation_options().unwrap();
5756 }
5757
5758 #[test]
5759 fn test_igvm_requires_firmware_and_personality() {
5760 for (options, missing) in [
5761 ("firmware=guest.igvm", "personality"),
5762 ("personality=openhcl", "firmware"),
5763 ] {
5764 for extra in [vec![], vec!["--hv", "--vtl2"]] {
5765 let mut args = vec!["openvmm", "--igvm", options];
5766 args.extend(extra);
5767 let err = Options::try_parse_from(args).err().unwrap();
5768 assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
5769 assert!(err.to_string().contains(missing));
5770 assert!(err.to_string().contains("required"));
5771 }
5772 }
5773 let opt = Options::try_parse_from(["openvmm"]).unwrap();
5774 opt.validate_igvm_options().unwrap();
5775 }
5776
5777 #[test]
5778 fn test_igvm_personality_values() {
5779 for (value, expected) in [
5780 ("openhcl", IgvmPersonalityCli::Openhcl),
5781 ("uefi", IgvmPersonalityCli::Uefi),
5782 ("linux-direct", IgvmPersonalityCli::LinuxDirect),
5783 ] {
5784 let options = format!("firmware=guest.igvm,personality={value}");
5785 let mut args = vec!["openvmm", "--igvm", &options];
5786 if expected == IgvmPersonalityCli::Openhcl {
5787 args.extend(["--hv", "--vtl2"]);
5788 }
5789 let opt = Options::try_parse_from(args).unwrap();
5790 opt.validate_igvm_options().unwrap();
5791 assert_eq!(
5792 opt.igvm,
5793 Some(IgvmCli {
5794 firmware: PathBuf::from("guest.igvm"),
5795 personality: expected,
5796 })
5797 );
5798 }
5799 }
5800
5801 #[test]
5802 fn test_igvm_options_accept_reordered_keys_and_windows_paths() {
5803 let opt = Options::try_parse_from([
5804 "openvmm",
5805 "--igvm",
5806 r"personality=uefi,firmware=C:\VM Images\guest=1.igvm",
5807 ])
5808 .unwrap();
5809 assert_eq!(
5810 opt.igvm,
5811 Some(IgvmCli {
5812 firmware: PathBuf::from(r"C:\VM Images\guest=1.igvm"),
5813 personality: IgvmPersonalityCli::Uefi,
5814 })
5815 );
5816 }
5817
5818 #[test]
5819 fn test_igvm_options_reject_invalid_values() {
5820 for options in [
5821 "",
5822 "guest.igvm",
5823 "firmware=,personality=uefi",
5824 "firmware=guest.igvm,personality=",
5825 "firmware=guest.igvm,personality=pcat",
5826 "firmware=guest.igvm,personality=UEFI",
5827 "firmware=guest.igvm,personality=uefi,unknown=on",
5828 "firmware=guest.igvm,firmware=other.igvm,personality=uefi",
5829 "firmware=guest.igvm,personality=uefi,personality=linux-direct",
5830 ] {
5831 let err = Options::try_parse_from(["openvmm", "--igvm", options])
5832 .err()
5833 .unwrap();
5834 assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
5835 }
5836 }
5837
5838 #[test]
5839 fn test_igvm_rejects_separate_personality_flag() {
5840 let err = Options::try_parse_from([
5841 "openvmm",
5842 "--igvm",
5843 "firmware=guest.igvm,personality=uefi",
5844 "--igvm-personality",
5845 "uefi",
5846 ])
5847 .err()
5848 .unwrap();
5849 assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
5850 }
5851
5852 #[test]
5853 fn test_openhcl_igvm_personality() {
5854 let opt = Options::try_parse_from([
5855 "openvmm",
5856 "--igvm",
5857 "firmware=guest.igvm,personality=openhcl",
5858 "--hv",
5859 "--vtl2",
5860 ])
5861 .unwrap();
5862 opt.validate_igvm_options().unwrap();
5863 assert_eq!(
5864 opt.igvm.as_ref().unwrap().personality,
5865 IgvmPersonalityCli::Openhcl
5866 );
5867 assert!(opt.effective_uefi().unwrap().is_none());
5868 }
5869
5870 #[test]
5871 fn test_openhcl_igvm_requires_explicit_vtl2_and_hv() {
5872 for extra in [vec![], vec!["--hv"], vec!["--vtl2"]] {
5873 let mut args = vec![
5874 "openvmm",
5875 "--igvm",
5876 "firmware=guest.igvm,personality=openhcl",
5877 ];
5878 args.extend(extra);
5879 match Options::try_parse_from(args) {
5880 Ok(opt) => assert_eq!(
5881 opt.validate_igvm_options().unwrap_err().to_string(),
5882 "--igvm personality=openhcl requires --hv --vtl2"
5883 ),
5884 Err(err) => {
5885 assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
5886 assert!(err.to_string().contains("--hv"));
5887 }
5888 }
5889 }
5890 }
5891
5892 #[test]
5893 fn test_non_openhcl_igvm_personalities_reject_vtl2_and_uefi() {
5894 for personality in ["uefi", "linux-direct"] {
5895 let options = format!("firmware=guest.igvm,personality={personality}");
5896 for extra in [vec!["--uefi"], vec!["--hv", "--vtl2"]] {
5897 let mut args = vec!["openvmm", "--igvm", &options];
5898 args.extend(extra);
5899 let opt = Options::try_parse_from(args).unwrap();
5900 assert!(opt.validate_igvm_options().is_err());
5901 }
5902 }
5903 }
5904
5905 #[test]
5906 fn test_openhcl_igvm_accepts_uefi_settings() {
5907 let opt = Options::try_parse_from([
5908 "openvmm",
5909 "--igvm",
5910 "firmware=guest.igvm,personality=openhcl",
5911 "--hv",
5912 "--vtl2",
5913 "--uefi",
5914 "console=com1,disable_frontpage",
5915 ])
5916 .unwrap();
5917 opt.validate_igvm_options().unwrap();
5918 let uefi = opt.effective_uefi().unwrap().unwrap();
5919 assert!(matches!(uefi.console, Some(UefiConsoleModeCli::Com1)));
5920 assert!(uefi.disable_frontpage);
5921 }
5922
5923 #[test]
5924 fn test_igvm_personality_conflicts_with_pcat() {
5925 for personality in ["openhcl", "uefi", "linux-direct"] {
5926 let options = format!("firmware=guest.igvm,personality={personality}");
5927 assert!(Options::try_parse_from(["openvmm", "--igvm", &options, "--pcat"]).is_err());
5928 }
5929 }
5930
5931 #[test]
5932 fn test_pidfile_option_parsed() {
5933 let opt = Options::try_parse_from(["openvmm", "--pidfile", "/tmp/test.pid"]).unwrap();
5934 assert_eq!(opt.pidfile, Some(PathBuf::from("/tmp/test.pid")));
5935 }
5936
5937 #[test]
5938 fn test_tpm_version_option() {
5939 let opt = Options::try_parse_from(["openvmm"]).unwrap();
5940 assert_eq!(opt.tpm, None);
5941
5942 let opt = Options::try_parse_from(["openvmm", "--tpm"]).unwrap();
5943 assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
5944
5945 let opt = Options::try_parse_from(["openvmm", "--tpm", "--uefi"]).unwrap();
5946 assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
5947 assert!(opt.uefi.is_some());
5948
5949 let opt = Options::try_parse_from(["openvmm", "--tpm", "138"]).unwrap();
5950 assert_eq!(opt.tpm, Some(TpmVersionCli::V138));
5951
5952 let opt = Options::try_parse_from(["openvmm", "--tpm=185"]).unwrap();
5953 assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
5954
5955 let opt = Options::try_parse_from(["openvmm", "--tpm", "1.38"]).unwrap();
5956 assert_eq!(opt.tpm, Some(TpmVersionCli::V138));
5957
5958 assert!(Options::try_parse_from(["openvmm", "--tpm", "137"]).is_err());
5959 }
5960
5961 #[test]
5962 fn test_uefi_options() {
5963 let options = Options::try_parse_from(["openvmm", "--uefi"]).unwrap();
5964 assert!(options.effective_uefi().unwrap().is_some());
5965
5966 let options = Options::try_parse_from([
5967 "openvmm",
5968 "--uefi",
5969 "firmware=foo/bar,debug,force_firmware_version,console=com1,diagnostics=full",
5970 ])
5971 .unwrap();
5972 let uefi = options.effective_uefi().unwrap().unwrap();
5973 assert_eq!(uefi.firmware, Some("foo/bar".into()));
5974 assert!(uefi.debug);
5975 assert!(uefi.force_firmware_version);
5976 assert!(matches!(uefi.console, Some(UefiConsoleModeCli::Com1)));
5977 assert!(matches!(
5978 uefi.diagnostics,
5979 Some(EfiDiagnosticsLogLevelCli::Full)
5980 ));
5981
5982 let options = Options::try_parse_from([
5983 "openvmm",
5984 "--uefi",
5985 "--uefi-firmware",
5986 "legacy.fd",
5987 "--uefi-debug",
5988 ])
5989 .unwrap();
5990 let uefi = options.effective_uefi().unwrap().unwrap();
5991 assert_eq!(uefi.firmware, Some("legacy.fd".into()));
5992 assert!(uefi.debug);
5993
5994 assert!(Options::try_parse_from(["openvmm", "--uefi", "unknown"]).is_err());
5995 let options = Options::try_parse_from(["openvmm", "--uefi", "debug=on"]).unwrap();
5996 assert!(options.effective_uefi().unwrap().unwrap().debug);
5997 let options = Options::try_parse_from(["openvmm", "--uefi", "debug=off"]).unwrap();
5998 assert!(!options.effective_uefi().unwrap().unwrap().debug);
5999 assert!(Options::try_parse_from(["openvmm", "--uefi", "debug,debug"]).is_err());
6000
6001 let options = Options::try_parse_from([
6002 "openvmm",
6003 "--uefi",
6004 "firmware=new.fd",
6005 "--uefi-firmware",
6006 "legacy.fd",
6007 ])
6008 .unwrap();
6009 assert!(options.effective_uefi().is_err());
6010 }
6011
6012 #[test]
6013 fn test_guest_power_action_flags() {
6014 let opt = Options::try_parse_from(["openvmm"]).unwrap();
6017 assert_eq!(opt.guest_reset_action, GuestPowerAction::Reset);
6018 assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Halt);
6019 assert_eq!(opt.guest_crash_action, GuestPowerAction::Halt);
6020 assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Reset);
6021 assert_eq!(
6024 crate::vm_controller::GuestPowerActions {
6025 shutdown: opt.guest_shutdown_action,
6026 reset: opt.guest_reset_action,
6027 crash: opt.guest_crash_action,
6028 watchdog: opt.guest_watchdog_action,
6029 },
6030 crate::vm_controller::GuestPowerActions::default(),
6031 );
6032
6033 let opt = Options::try_parse_from([
6034 "openvmm",
6035 "--guest-watchdog",
6036 "--guest-reset-action",
6037 "exit",
6038 "--guest-shutdown-action",
6039 "exit:5",
6040 "--guest-crash-action",
6041 "reset",
6042 "--guest-watchdog-action",
6043 "halt",
6044 ])
6045 .unwrap();
6046 assert_eq!(opt.guest_reset_action, GuestPowerAction::Exit(0));
6048 assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Exit(5));
6049 assert_eq!(opt.guest_crash_action, GuestPowerAction::Reset);
6050 assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Halt);
6051
6052 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:nope"]).is_err());
6054 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:300"]).is_err());
6055 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:-1"]).is_err());
6056
6057 assert!(Options::try_parse_from(["openvmm", "--guest-watchdog-action", "halt"]).is_err());
6059 }
6060
6061 #[cfg(target_os = "linux")]
6062 #[test]
6063 fn test_vfio_device_cli_parse() {
6064 use vfio_assigned_device_resources::BarAddressConfig;
6065
6066 let v = VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0").unwrap();
6068 assert_eq!(v.pci_id, "0000:01:00.0");
6069 assert_eq!(v.port_name, "rp0");
6070 assert_eq!(v.iommu, None);
6071
6072 let v = VfioDeviceCli::from_str("port=rp1,iommu=iommu0,host=0000:02:00.0").unwrap();
6074 assert_eq!(v.pci_id, "0000:02:00.0");
6075 assert_eq!(v.port_name, "rp1");
6076 assert_eq!(v.iommu.as_deref(), Some("iommu0"));
6077
6078 let v = VfioDeviceCli::from_str(
6079 "host=0000:03:00.0,port=rp2,bar0=host,bar2=0x80000000,bar4=0x110000000000",
6080 )
6081 .unwrap();
6082 assert_eq!(v.bar_addresses[0], BarAddressConfig::HostAssigned);
6083 assert_eq!(v.bar_addresses[1], BarAddressConfig::GuestAssigned);
6084 assert_eq!(v.bar_addresses[2], BarAddressConfig::Fixed(0x80000000));
6085 assert_eq!(v.bar_addresses[4], BarAddressConfig::Fixed(0x110000000000));
6086 }
6087
6088 #[cfg(target_os = "linux")]
6089 #[test]
6090 fn test_vfio_device_cli_errors() {
6091 assert!(VfioDeviceCli::from_str("port=rp0").is_err());
6093 assert!(VfioDeviceCli::from_str("host=0000:01:00.0").is_err());
6094
6095 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,foo=bar").is_err());
6097
6098 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,host=0000:02:00.0,port=rp0").is_err());
6100 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,port=rp1").is_err());
6101 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=a,iommu=b").is_err());
6102
6103 assert!(VfioDeviceCli::from_str("host=,port=rp0").is_err());
6105 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=").is_err());
6106 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=").is_err());
6107
6108 assert!(VfioDeviceCli::from_str("host").is_err());
6110 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu").is_err());
6111
6112 assert!(VfioDeviceCli::from_str("host=../../etc/passwd,port=rp0").is_err());
6114 assert!(VfioDeviceCli::from_str("host=foo/bar,port=rp0").is_err());
6115
6116 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0").is_err());
6118 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x0").is_err());
6119 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0xnope").is_err());
6120 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=pt").is_err());
6121 assert!(
6122 VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x1000,bar0=host").is_err()
6123 );
6124 }
6125
6126 #[cfg(target_os = "linux")]
6127 #[test]
6128 fn test_iommu_cli_parse() {
6129 let c = IommuCli::from_str("id=iommu0").unwrap();
6130 assert_eq!(c.id, "iommu0");
6131
6132 assert!(IommuCli::from_str("name=iommu0").is_err());
6134
6135 assert!(IommuCli::from_str("iommu0").is_err());
6137
6138 assert!(IommuCli::from_str("id=").is_err());
6140 }
6141
6142 #[cfg(target_os = "linux")]
6143 #[test]
6144 fn test_vhost_user_cli() {
6145 let v = VhostUserCli::from_str("/run/blk.sock,type=blk,num_queues=2").unwrap();
6147 assert_eq!(v.socket_path, "/run/blk.sock");
6148 assert!(matches!(
6149 v.device_type,
6150 VhostUserDeviceTypeCli::Blk {
6151 num_queues: Some(2),
6152 queue_size: None
6153 }
6154 ));
6155 assert_eq!(v.pcie_port, None);
6156
6157 let v = VhostUserCli::from_str("/run/fs.sock,type=fs,tag=myfs,pcie_port=p0").unwrap();
6159 assert!(matches!(
6160 &v.device_type,
6161 VhostUserDeviceTypeCli::Fs { tag, .. } if tag == "myfs"
6162 ));
6163 assert_eq!(v.pcie_port.as_deref(), Some("p0"));
6164
6165 let v = VhostUserCli::from_str("/run/x.sock,device_id=9,queue_sizes=[16,32]").unwrap();
6167 assert!(matches!(
6168 &v.device_type,
6169 VhostUserDeviceTypeCli::Other { device_id: 9, queue_sizes } if *queue_sizes == vec![16, 32]
6170 ));
6171
6172 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()); }
6183
6184 #[cfg(target_os = "linux")]
6185 #[test]
6186 fn test_vhost_vsock_cli() {
6187 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "3"]).unwrap();
6188 assert_eq!(opt.virtio_vsock_vhost_cid, Some(3));
6189
6190 let opt =
6191 Options::try_parse_from(["openvmm", "--virtio-vsock-path", "/tmp/vsock"]).unwrap();
6192 assert_eq!(opt.virtio_vsock_path.as_deref(), Some("/tmp/vsock"));
6193
6194 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "2"]).is_err());
6195 assert!(
6196 Options::try_parse_from([
6197 "openvmm",
6198 "--virtio-vsock-vhost-cid",
6199 "3",
6200 "--virtio-vsock-path",
6201 "/tmp/vsock",
6202 ])
6203 .is_err()
6204 );
6205 }
6206
6207 #[test]
6208 fn test_virtio_vsock_bus_cli() {
6209 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "mmio"]).unwrap();
6210 assert_eq!(opt.virtio_vsock_bus, Some(VirtioBusCli::Mmio));
6211
6212 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pci"]).unwrap();
6213 assert_eq!(opt.virtio_vsock_bus, Some(VirtioBusCli::Pci));
6214
6215 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie"]).unwrap();
6216 assert_eq!(
6217 opt.virtio_vsock_bus,
6218 Some(VirtioBusCli::Pcie("vsock".to_string()))
6219 );
6220
6221 let opt =
6222 Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie:custom"]).unwrap();
6223 assert_eq!(
6224 opt.virtio_vsock_bus,
6225 Some(VirtioBusCli::Pcie("custom".to_string()))
6226 );
6227
6228 let opt =
6229 Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "PCIE:custom"]).unwrap();
6230 assert_eq!(
6231 opt.virtio_vsock_bus,
6232 Some(VirtioBusCli::Pcie("custom".to_string()))
6233 );
6234
6235 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "auto"]).is_err());
6236 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "vpci"]).is_err());
6237 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie:"]).is_err());
6238 assert!(
6239 Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie:custom:extra"])
6240 .is_err()
6241 );
6242
6243 let opt =
6244 Options::try_parse_from(["openvmm", "--virtio-rng", "--virtio-rng-bus", "pcie:custom"])
6245 .unwrap();
6246 assert_eq!(opt.virtio_rng_bus, VirtioBusCli::Pcie("custom".to_string()));
6247 }
6248
6249 #[test]
6250 fn test_virtio_rtc_cli() {
6251 let opt = Options::try_parse_from(["openvmm"]).unwrap();
6252 assert!(opt.virtio_rtc.is_none());
6253
6254 let opt =
6255 Options::try_parse_from(["openvmm", "--virtio-rtc", "--processors", "2"]).unwrap();
6256 assert_eq!(
6257 opt.virtio_rtc,
6258 Some(VirtioRtcCli {
6259 bus: VirtioBusCli::Auto
6260 })
6261 );
6262
6263 let opt = Options::try_parse_from(["openvmm", "--virtio-rtc", "bus=pcie:rp0"]).unwrap();
6264 assert_eq!(
6265 opt.virtio_rtc,
6266 Some(VirtioRtcCli {
6267 bus: VirtioBusCli::Pcie("rp0".to_string())
6268 })
6269 );
6270
6271 for value in ["mmio", "bus", "bus=", "bus=pcie:", "bus=pcie:rp0,bus=mmio"] {
6272 assert!(Options::try_parse_from(["openvmm", "--virtio-rtc", value]).is_err());
6273 }
6274 }
6275
6276 #[test]
6277 fn test_nvme_controller_cli_pcie() {
6278 let c = NvmeControllerCli::from_str("id=nvme0,pcie_port=p0").unwrap();
6279 assert_eq!(c.id, "nvme0");
6280 assert_eq!(c.transport, NvmeControllerTransport::Pcie("p0".into()));
6281 }
6282
6283 #[test]
6284 fn test_nvme_controller_cli_vpci_no_guid() {
6285 let c = NvmeControllerCli::from_str("id=nvme1,vpci").unwrap();
6286 assert_eq!(c.id, "nvme1");
6287 assert!(matches!(c.transport, NvmeControllerTransport::Vpci(None)));
6288 assert!(NvmeControllerCli::from_str("id=nvme1,vpci=").is_err());
6289 }
6290
6291 #[test]
6292 fn test_nvme_controller_cli_vpci_with_guid() {
6293 let c = NvmeControllerCli::from_str("id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c")
6294 .unwrap();
6295 assert_eq!(c.id, "nvme2");
6296 assert!(matches!(
6297 c.transport,
6298 NvmeControllerTransport::Vpci(Some(_))
6299 ));
6300 }
6301
6302 #[test]
6303 fn test_nvme_controller_cli_errors() {
6304 assert!(NvmeControllerCli::from_str("pcie_port=p0").is_err());
6306 assert!(NvmeControllerCli::from_str("id=nvme0").is_err());
6308 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,vpci").is_err());
6310 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,foo=bar").is_err());
6312 assert!(NvmeControllerCli::from_str("id=,pcie_port=p0").is_err());
6314 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=").is_err());
6316 let err = NvmeControllerCli::from_str("id=nvme0,vpci=not-a-guid").unwrap_err();
6318 assert!(err.to_string().contains("invalid value for option 'vpci'"));
6319 }
6320
6321 #[test]
6322 fn test_disk_cli_controller() {
6323 let d = DiskCli::from_str("file:disk.vhd,on=nvme0").unwrap();
6324 assert_eq!(d.controller.as_deref(), Some("nvme0"));
6325 assert_eq!(d.nsid, None);
6326 }
6327
6328 #[test]
6329 fn test_disk_cli_serial() {
6330 let d = DiskCli::from_str("file:disk.vhd,serial=DATA-DISK").unwrap();
6331 assert_eq!(d.serial.as_deref(), Some("DATA-DISK"));
6332 assert!(DiskCli::from_str("file:disk.vhd,serial=").is_err());
6333 assert!(DiskCli::from_str("file:disk.vhd,serial=DATA[1]").is_err());
6334 assert!(DiskCli::from_str("file:disk.vhd,serial=DATA,DISK").is_err());
6335 assert!(DiskCli::from_str("file:disk.vhd,serial=DATA\u{1f}DISK").is_err());
6336 assert!(DiskCli::from_str("file:disk.vhd,serial=non-ascii-\u{e9}").is_err());
6337 }
6338
6339 #[test]
6340 fn test_disk_cli_controller_with_nsid() {
6341 let d = DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=3").unwrap();
6342 assert_eq!(d.controller.as_deref(), Some("nvme0"));
6343 assert_eq!(d.nsid, Some(3));
6344 }
6345
6346 #[test]
6347 fn test_disk_cli_controller_errors() {
6348 assert!(DiskCli::from_str("file:disk.vhd,nsid=1").is_err());
6350 assert!(DiskCli::from_str("file:disk.vhd,lun=0").is_err());
6352 assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,pcie_port=p0").is_err());
6354 assert!(DiskCli::from_str("file:disk.vhd,on=").is_err());
6356 assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=abc").is_err());
6358 assert!(DiskCli::from_str("file:disk.vhd,on=c,nsid=1,lun=0").is_err());
6360 }
6361
6362 #[test]
6363 fn test_disk_cli_controller_with_lun() {
6364 let d = DiskCli::from_str("file:disk.vhd,on=scsi0,lun=3").unwrap();
6365 assert_eq!(d.controller.as_deref(), Some("scsi0"));
6366 assert_eq!(d.lun, Some(3));
6367 assert_eq!(d.nsid, None);
6368 }
6369
6370 #[test]
6371 fn test_scsi_controller_cli() {
6372 let c = ScsiControllerCli::from_str("id=scsi0").unwrap();
6373 assert_eq!(c.id, "scsi0");
6374 assert_eq!(c.sub_channels, 0);
6375 }
6376
6377 #[test]
6378 fn test_scsi_controller_cli_with_sub_channels() {
6379 let c = ScsiControllerCli::from_str("id=scsi1,sub_channels=4").unwrap();
6380 assert_eq!(c.id, "scsi1");
6381 assert_eq!(c.sub_channels, 4);
6382 }
6383
6384 #[test]
6385 fn test_scsi_controller_cli_errors() {
6386 assert!(ScsiControllerCli::from_str("sub_channels=4").is_err());
6388 assert!(ScsiControllerCli::from_str("id=").is_err());
6390 assert!(ScsiControllerCli::from_str("id=scsi0,foo=bar").is_err());
6392 assert!(ScsiControllerCli::from_str("id=scsi0,sub_channels=abc").is_err());
6394 }
6395
6396 #[test]
6397 fn test_disk_cli_relay() {
6398 let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt").unwrap();
6399 assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
6400 assert_eq!(d.relay.as_ref().unwrap().1, None);
6401 }
6402
6403 #[test]
6404 fn test_disk_cli_relay_with_location() {
6405 let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:3").unwrap();
6406 assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
6407 assert_eq!(d.relay.as_ref().unwrap().1, Some(3));
6408 }
6409
6410 #[test]
6411 fn test_disk_cli_relay_errors() {
6412 assert!(DiskCli::from_str("file:disk.vhd,relay=tgt").is_err());
6414 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt,uh").is_err());
6416 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:abc").is_err());
6418 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=").is_err());
6420 }
6421
6422 #[test]
6423 fn test_nvme_controller_cli_vtl2() {
6424 let c = NvmeControllerCli::from_str("id=nvme0,vpci,vtl2").unwrap();
6425 assert_eq!(c.vtl, DeviceVtl::Vtl2);
6426 }
6427
6428 #[test]
6429 fn test_scsi_controller_cli_vtl2() {
6430 let c = ScsiControllerCli::from_str("id=scsi0,vtl2").unwrap();
6431 assert_eq!(c.vtl, DeviceVtl::Vtl2);
6432 }
6433
6434 #[test]
6435 fn test_openhcl_controller_cli() {
6436 let c = OpenhclControllerCli::from_str("id=vtl0-scsi,type=scsi").unwrap();
6437 assert_eq!(c.id, "vtl0-scsi");
6438 assert_eq!(c.controller_type, OpenhclControllerType::Scsi);
6439 assert_eq!(c.guid, None);
6440 }
6441
6442 #[test]
6443 fn test_openhcl_controller_cli_nvme_with_guid() {
6444 let c = OpenhclControllerCli::from_str(
6445 "id=vtl0-nvme,type=nvme,guid=09a59b81-2bf6-4164-81d7-3a0dc977ba65",
6446 )
6447 .unwrap();
6448 assert_eq!(c.controller_type, OpenhclControllerType::Nvme);
6449 assert!(c.guid.is_some());
6450 }
6451
6452 #[test]
6453 fn test_openhcl_controller_cli_errors() {
6454 assert!(OpenhclControllerCli::from_str("type=scsi").is_err());
6456 assert!(OpenhclControllerCli::from_str("id=foo").is_err());
6458 assert!(OpenhclControllerCli::from_str("id=foo,type=ide").is_err());
6460 assert!(OpenhclControllerCli::from_str("id=foo,type=scsi,guid=bad").is_err());
6462 }
6463
6464 #[test]
6465 fn test_parse_vp_list() {
6466 use vmm_cli::BracketRangeList;
6467
6468 let parse = |s: &str| {
6469 s.parse::<BracketRangeList>()
6470 .and_then(|v| v.expand_below(1024))
6471 };
6472
6473 assert_eq!(parse("[0,1,2,3]").unwrap(), vec![0, 1, 2, 3]);
6475
6476 assert_eq!(parse("[5]").unwrap(), vec![5]);
6478
6479 assert_eq!(parse("[0-3]").unwrap(), vec![0, 1, 2, 3]);
6481
6482 assert_eq!(parse("[0,1,4-6,10]").unwrap(), vec![0, 1, 4, 5, 6, 10]);
6484
6485 assert_eq!(parse("[0, 1, 2-4]").unwrap(), vec![0, 1, 2, 3, 4]);
6487
6488 assert!(parse("0,1,2").is_err());
6490 assert!(parse("0-3").is_err());
6491
6492 assert!(parse("[3-0]").is_err());
6494
6495 assert!(parse("[a,b]").is_err());
6497 }
6498
6499 #[test]
6500 fn test_parse_numa_node() {
6501 use super::parse_numa_node;
6502
6503 let n = parse_numa_node("size=2G").unwrap();
6505 assert_eq!(
6506 n.memory.size,
6507 Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024))
6508 );
6509 assert!(n.vps.is_none());
6510 assert!(n.host_numa_node.is_none());
6511
6512 let n = parse_numa_node("size=1G,vps=[0,1,2,3]").unwrap();
6514 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
6515
6516 let n = parse_numa_node("size=1G,vps=[0-3]").unwrap();
6518 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
6519
6520 let n = parse_numa_node("size=1G,host_numa_node=1").unwrap();
6522 assert_eq!(n.host_numa_node, Some(1));
6523
6524 let n = parse_numa_node("size=1G,vps=[0,1],host_numa_node=0,hugepages=on").unwrap();
6526 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1]);
6527 assert_eq!(n.host_numa_node, Some(0));
6528 assert!(n.memory.hugepages);
6529
6530 assert!(parse_numa_node("vps=[0,1]").is_err());
6532
6533 assert!(parse_numa_node("size=1G,vps=0,1").is_err());
6535
6536 assert!(parse_numa_node("size=1G,vps=[0],vps=[1]").is_err());
6538
6539 assert!(parse_numa_node("size=1G,file=/tmp/x").is_err());
6541
6542 let n = parse_numa_node("size=1G,vps=[]").unwrap();
6544 assert!(n.vps.unwrap().0.is_empty());
6545 }
6546
6547 #[test]
6548 fn test_parse_numa_distance() {
6549 use super::parse_numa_distance;
6550
6551 let d = parse_numa_distance("0:1:20").unwrap();
6552 assert_eq!(d.src, 0);
6553 assert_eq!(d.dst, 1);
6554 assert_eq!(d.distance, 20);
6555
6556 let d = parse_numa_distance("0:0:10").unwrap();
6558 assert_eq!(d.distance, 10);
6559
6560 assert!(parse_numa_distance("0:1:5").is_err());
6562
6563 assert!(parse_numa_distance("0:1").is_err());
6565 assert!(parse_numa_distance("0:1:20:extra").is_err());
6566 }
6567
6568 #[cfg(guest_arch = "aarch64")]
6569 #[test]
6570 fn test_smmu_cli_from_str() {
6571 let s = SmmuCli::from_str("rc=pcie0").unwrap();
6573 assert_eq!(s.rc_name, "pcie0");
6574 assert!(!s.accel);
6575 assert!(matches!(s.oas, SmmuOasCli::Auto));
6576
6577 let s = SmmuCli::from_str("rc=pcie0,accel").unwrap();
6579 assert_eq!(s.rc_name, "pcie0");
6580 assert!(s.accel);
6581 assert!(matches!(s.oas, SmmuOasCli::Auto));
6582
6583 let s = SmmuCli::from_str("rc=pcie0,oas=auto").unwrap();
6585 assert!(matches!(s.oas, SmmuOasCli::Auto));
6586
6587 let s = SmmuCli::from_str("rc=pcie0,oas=52").unwrap();
6589 assert!(matches!(s.oas, SmmuOasCli::Fixed(52)));
6590
6591 let s = SmmuCli::from_str("oas=48,accel,rc=pcie1").unwrap();
6593 assert_eq!(s.rc_name, "pcie1");
6594 assert!(s.accel);
6595 assert!(matches!(s.oas, SmmuOasCli::Fixed(48)));
6596
6597 assert!(SmmuCli::from_str("accel").is_err());
6599 assert!(SmmuCli::from_str("oas=52").is_err());
6600
6601 assert!(SmmuCli::from_str("rc=").is_err());
6603
6604 assert!(SmmuCli::from_str("rc=pcie0,rc=pcie1").is_err());
6606
6607 assert!(SmmuCli::from_str("rc=pcie0,oas=big").is_err());
6609
6610 assert!(SmmuCli::from_str("rc=pcie0,foo=bar").is_err());
6612
6613 assert!(SmmuCli::from_str("rc=pcie0,turbo").is_err());
6615 }
6616}