1#![warn(missing_docs)]
20
21use anyhow::Context;
22use clap::Parser;
23use clap::ValueEnum;
24use cxl_spec::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"#)]
567 #[clap(long = "virtio-blk")]
568 pub virtio_blk: Vec<DiskCli>,
569
570 #[cfg(target_os = "linux")]
595 #[clap(long = "vhost-user")]
596 pub vhost_user: Vec<VhostUserCli>,
597
598 #[clap(long, value_name = "COUNT", default_value = "0")]
600 pub scsi_sub_channels: u16,
601
602 #[clap(long)]
604 pub nic: bool,
605
606 #[clap(long)]
618 pub net: Vec<NicConfigCli>,
619
620 #[clap(long, value_name = "SWITCH_ID")]
624 pub kernel_vmnic: Vec<String>,
625
626 #[clap(long)]
628 pub gfx: bool,
629
630 #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
632 pub vtl2_gfx: bool,
633
634 #[clap(flatten)]
636 pub vnc: VncCli,
637
638 #[cfg(guest_arch = "x86_64")]
640 #[clap(long, default_value_t)]
641 pub apic_id_offset: u32,
642
643 #[clap(long)]
645 pub vps_per_socket: Option<u32>,
646
647 #[clap(long, default_value = "auto")]
649 pub smt: SmtConfigCli,
650
651 #[cfg(guest_arch = "x86_64")]
653 #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
654 pub x2apic: X2ApicConfig,
655
656 #[cfg(guest_arch = "aarch64")]
658 #[clap(long, default_value = "auto")]
659 pub gic_msi: GicMsiCli,
660
661 #[cfg(guest_arch = "aarch64")]
665 #[clap(long, value_name = "SMMU_CONFIG")]
666 pub smmu: Vec<SmmuCli>,
667
668 #[clap(long, value_name = "SERIAL")]
680 pub com1: Option<ComSerialConfigCli>,
681
682 #[clap(long, value_name = "SERIAL")]
685 pub com2: Option<ComSerialConfigCli>,
686
687 #[clap(long, value_name = "SERIAL")]
690 pub com3: Option<ComSerialConfigCli>,
691
692 #[clap(long, value_name = "SERIAL")]
695 pub com4: Option<ComSerialConfigCli>,
696
697 #[structopt(long, value_name = "SERIAL")]
699 pub vmbus_com1_serial: Option<SerialConfigCli>,
700
701 #[structopt(long, value_name = "SERIAL")]
703 pub vmbus_com2_serial: Option<SerialConfigCli>,
704
705 #[clap(long)]
707 pub serial_tx_only: bool,
708
709 #[clap(long, value_name = "SERIAL")]
711 pub debugcon: Option<DebugconSerialConfigCli>,
712
713 #[clap(long, short = 'e')]
715 pub uefi: bool,
716
717 #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
719 pub uefi_firmware: OptionalPathBuf,
720
721 #[clap(long, requires("uefi"))]
723 pub uefi_debug: bool,
724
725 #[clap(long, requires("uefi"))]
727 pub uefi_enable_memory_protections: bool,
728
729 #[clap(long, requires("uefi"))]
731 pub uefi_force_dma_bounce: bool,
732
733 #[clap(long, requires("pcat"))]
744 pub pcat_boot_order: Option<PcatBootOrderCli>,
745
746 #[clap(long, conflicts_with("uefi"))]
748 pub pcat: bool,
749
750 #[clap(long, requires("pcat"), value_name = "FILE")]
752 pub pcat_firmware: Option<PathBuf>,
753
754 #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
756 pub igvm: Option<PathBuf>,
757
758 #[clap(
760 long,
761 requires("igvm"),
762 conflicts_with_all = ["vtl2", "uefi", "pcat"],
763 value_enum
764 )]
765 pub igvm_personality: Option<IgvmPersonalityCli>,
766
767 #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
770 pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
771
772 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
777 pub virtio_9p: Vec<FsArgs>,
778
779 #[clap(long)]
781 pub virtio_9p_debug: bool,
782
783 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
788 pub virtio_fs: Vec<FsArgsWithOptions>,
789
790 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
795 pub virtio_fs_shmem: Vec<FsArgs>,
796
797 #[clap(long, value_name = "BUS", default_value = "auto")]
799 pub virtio_fs_bus: VirtioBusCli,
800
801 #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
806 pub virtio_pmem: Option<VirtioPmemArgs>,
807
808 #[clap(long)]
810 pub virtio_rng: bool,
811
812 #[clap(long, value_name = "BUS", default_value = "auto")]
814 pub virtio_rng_bus: VirtioBusCli,
815
816 #[clap(long, value_name = "PORT", requires("virtio_rng"))]
818 pub virtio_rng_pcie_port: Option<String>,
819
820 #[clap(long)]
826 pub virtio_console: Option<SerialConfigCli>,
827
828 #[clap(long, value_name = "PORT", requires("virtio_console"))]
830 pub virtio_console_pcie_port: Option<String>,
831
832 #[clap(long, value_name = "BUS", value_parser = parse_virtio_vsock_bus)]
834 pub virtio_vsock_bus: Option<VirtioBusCli>,
835
836 #[clap(long, value_name = "PATH")]
838 pub virtio_vsock_path: Option<String>,
839
840 #[cfg(target_os = "linux")]
843 #[clap(
844 long,
845 value_name = "CID",
846 conflicts_with = "virtio_vsock_path",
847 value_parser = parse_vhost_vsock_cid
848 )]
849 pub virtio_vsock_vhost_cid: Option<u32>,
850
851 #[clap(long)]
858 pub virtio_net: Vec<NicConfigCli>,
859
860 #[clap(long, value_name = "PATH")]
862 pub log_file: Option<PathBuf>,
863
864 #[clap(long, value_name = "PATH")]
868 pub pidfile: Option<PathBuf>,
869
870 #[clap(long, value_name = "SOCKETPATH")]
874 pub ttrpc: Option<PathBuf>,
875
876 #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
880 pub grpc: Option<PathBuf>,
881
882 #[clap(long_help = r#"
884Run as an RPC server on the specified Unix socket.
885
886syntax: path=<PATH>[,transport=<TRANSPORT>]
887
888options:
889 `path=<PATH>` Unix socket path to listen on (required)
890 `transport=<TRANSPORT>` wire transport to accept (default: auto)
891
892valid transports:
893 `auto` auto-detect ttrpc vs. gRPC per connection
894 `ttrpc` accept ttrpc clients only
895 `grpc` accept gRPC clients only
896
897Examples:
898 --rpc path=/tmp/openvmm.sock
899 --rpc path=/tmp/openvmm.sock,transport=ttrpc
900"#)]
901 #[clap(
902 long,
903 value_name = "path=PATH[,transport=TRANSPORT]",
904 conflicts_with("ttrpc"),
905 conflicts_with("grpc")
906 )]
907 pub rpc: Option<RpcCli>,
908
909 #[clap(long)]
911 pub single_process: bool,
912
913 #[cfg(windows)]
915 #[clap(long, value_name = "PATH")]
916 pub device: Vec<String>,
917
918 #[clap(long, requires("uefi"))]
920 pub disable_frontpage: bool,
921
922 #[clap(
924 long,
925 value_name = "VERSION",
926 num_args = 0..=1,
927 default_missing_value = "185"
928 )]
929 pub tpm: Option<TpmVersionCli>,
930
931 #[clap(long, default_value = "control", hide(true))]
935 #[expect(clippy::option_option)]
936 pub internal_worker: Option<Option<String>>,
937
938 #[clap(long, requires("vtl2"))]
940 pub vmbus_redirect: bool,
941
942 #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
944 pub vmbus_max_version: Option<u32>,
945
946 #[clap(long_help = r#"
950e.g: --vmgs memdiff:file:/path/to/file.vmgs
951
952syntax: <path> | kind:<arg>[,flag]
953
954valid disk kinds:
955 `mem:<len>` memory backed disk
956 <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
957 `memdiff:<disk>[;create=<len>]` memory backed diff disk
958 <disk>: lower disk, e.g.: `file:base.img`
959 `file:<path>` file-backed disk
960 <path>: path to file
961
962flags:
963 `fmt` reprovision the VMGS before boot
964 `fmt-on-fail` reprovision the VMGS before boot if it is corrupted
965"#)]
966 #[clap(long)]
967 pub vmgs: Option<VmgsCli>,
968
969 #[clap(long, requires("vmgs"))]
971 pub test_gsp_by_id: bool,
972
973 #[clap(long, requires("pcat"), value_name = "FILE")]
975 pub vga_firmware: Option<PathBuf>,
976
977 #[clap(long)]
979 pub secure_boot: bool,
980
981 #[clap(long)]
983 pub secure_boot_template: Option<SecureBootTemplateCli>,
984
985 #[clap(long, value_name = "PATH")]
987 pub custom_uefi_json: Option<PathBuf>,
988
989 #[clap(long, hide(true))]
994 pub relay_console_path: Option<PathBuf>,
995
996 #[clap(long, hide(true))]
1000 pub relay_console_title: Option<String>,
1001
1002 #[clap(long, value_name = "PORT")]
1004 pub gdb: Option<u16>,
1005
1006 #[clap(long)]
1011 pub mana: Vec<NicConfigCli>,
1012
1013 #[clap(long)]
1028 pub hypervisor: Option<String>,
1029
1030 #[clap(long)]
1036 pub nested_virt: bool,
1037
1038 #[clap(long_help = r#"
1048e.g: --ide memdiff:file:/path/to/disk.vhd
1049
1050syntax: <path> | kind:<arg>[,flag,opt=arg,...]
1051
1052valid disk kinds:
1053 `mem:<len>` memory backed disk
1054 <len>: length of ramdisk, e.g.: `1G`
1055 `memdiff:<disk>` memory backed diff disk
1056 <disk>: lower disk, e.g.: `file:base.img`
1057 `file:<path>[;create=<len>]` file-backed disk
1058 <path>: path to file
1059 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
1060 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
1061 `blob:<type>:<url>` HTTP blob (read-only)
1062 <type>: `flat` or `vhd1`
1063 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
1064 <cipher>: `xts-aes-256`
1065
1066additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
1067this list is not exhaustive.
1068
1069flags:
1070 `ro` open disk as read-only
1071 `s` attach drive to secondary ide channel
1072 `dvd` specifies that device is cd/dvd and it is read_only
1073"#)]
1074 #[clap(long, value_name = "FILE", requires("pcat"))]
1075 pub ide: Vec<IdeDiskCli>,
1076
1077 #[clap(long_help = r#"
1080e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
1081
1082syntax: <path> | kind:<arg>[,flag,opt=arg,...]
1083
1084valid disk kinds:
1085 `mem:<len>` memory backed disk
1086 <len>: length of ramdisk, e.g.: `1G`
1087 `memdiff:<disk>` memory backed diff disk
1088 <disk>: lower disk, e.g.: `file:base.img`
1089 `file:<path>[;create=<len>]` file-backed disk
1090 <path>: path to file
1091 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
1092 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
1093 `blob:<type>:<url>` HTTP blob (read-only)
1094 <type>: `flat` or `vhd1`
1095 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
1096 <cipher>: `xts-aes-256`
1097
1098flags:
1099 `ro` open disk as read-only
1100"#)]
1101 #[clap(long, value_name = "FILE", requires("pcat"))]
1102 pub floppy: Vec<FloppyDiskCli>,
1103
1104 #[clap(long)]
1106 pub guest_watchdog: bool,
1107
1108 #[clap(long)]
1111 pub openhcl_dump_path: Option<PathBuf>,
1112
1113 #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action)]
1117 pub guest_reset_action: GuestPowerAction,
1118
1119 #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1123 pub guest_shutdown_action: GuestPowerAction,
1124
1125 #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1129 pub guest_crash_action: GuestPowerAction,
1130
1131 #[clap(long, value_name = "PATH")]
1139 pub crash_dump_path: Option<PathBuf>,
1140
1141 #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action, requires = "guest_watchdog")]
1145 pub guest_watchdog_action: GuestPowerAction,
1146
1147 #[clap(long)]
1149 pub write_saved_state_proto: Option<PathBuf>,
1150
1151 #[clap(long)]
1153 pub imc: Option<PathBuf>,
1154
1155 #[clap(long)]
1157 pub battery: bool,
1158
1159 #[clap(long)]
1161 pub hibernation: bool,
1162
1163 #[clap(long)]
1165 pub uefi_console_mode: Option<UefiConsoleModeCli>,
1166
1167 #[clap(long_help = r#"
1169Set the EFI diagnostics log level.
1170
1171options:
1172 default default (ERROR and WARN only)
1173 info info (ERROR, WARN, and INFO)
1174 full full (all log levels)
1175"#)]
1176 #[clap(long, requires("uefi"))]
1177 pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
1178
1179 #[clap(long)]
1181 pub default_boot_always_attempt: bool,
1182
1183 #[cfg(guest_arch = "x86_64")]
1189 #[clap(long)]
1190 pub amd_iommu: Vec<String>,
1191
1192 #[cfg(guest_arch = "x86_64")]
1198 #[clap(long)]
1199 pub intel_vtd: Vec<String>,
1200
1201 #[clap(long_help = r#"
1203Attach root complexes to the VM.
1204
1205Examples:
1206 # Attach root complex rc0 on segment 0 with bus and MMIO ranges
1207 --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
1208
1209 # Configure HDM window size and restrictions (bitmask)
1210 --pcie-root-complex rc1,hdm=2G,hdm_window_restrictions=0x21
1211
1212Syntax: <name>[,opt=arg,...]
1213
1214Options:
1215 `segment=<value>` configures the PCI Express segment, default 0
1216 `start_bus=<value>` lowest valid bus number, default 0
1217 `end_bus=<value>` highest valid bus number, default 255
1218 `low_mmio=<size>` low MMIO window size, default 64M
1219 `high_mmio=<size>` high MMIO window size, default 1G
1220 `low_mmio_base=<addr>` pin low MMIO window base address (0x-prefixed hex)
1221 `high_mmio_base=<addr>` pin high MMIO window base address (0x-prefixed hex)
1222 `hdm=<size>` HDM decoder MMIO window size (CFMWS window), default 1G
1223 `hdm_window_restrictions=<m>` CFMWS window restriction bitmask (u16, decimal or 0x-prefixed hex),
1224 default DEVICE_COHERENT (bit 0, value 0x1)
1225 `preserve_bars` keep pinned BARs at their assigned addresses
1226 `node=<value>` NUMA node the root complex is associated with
1227"#)]
1228 #[clap(long, conflicts_with("pcat"))]
1229 pub pcie_root_complex: Vec<PcieRootComplexCli>,
1230
1231 #[clap(long, requires("pcie_root_complex"), conflicts_with("pcat"))]
1233 pub pcie_ecam_below_4gb: bool,
1234
1235 #[clap(long_help = r#"
1237Attach root ports to root complexes.
1238
1239Examples:
1240 # Attach root port rc0rp0 to root complex rc0
1241 --pcie-root-port rc0:rc0rp0
1242
1243 # Attach root port rc0rp1 to root complex rc0 with hotplug support
1244 --pcie-root-port rc0:rc0rp1,hotplug
1245
1246 # Attach root port rc0rp2 at device 5, function 0
1247 --pcie-root-port rc0:rc0rp2,addr=5
1248
1249 # Attach root port rc0rp3 at device 5, function 1
1250 --pcie-root-port rc0:rc0rp3,addr=5.1
1251
1252Syntax: <root_complex_name>:<name>[,opt,opt=arg,...]
1253
1254Options:
1255 `addr=<dev>[.<fn>]` device/function to place this port at (default:
1256 lowest available); dev 0-31, fn 0-7
1257 `hotplug` enable hotplug support for this root port
1258 `acs=<mask>` ACS capability bitmask (u16, decimal or 0x-prefixed hex)
1259 `cxl` configure this root port as CXL-capable
1260 `pasid` configure this port to support PASID for downstream devices
1261"#)]
1262 #[clap(long, conflicts_with("pcat"))]
1263 pub pcie_root_port: Vec<PcieRootPortCli>,
1264
1265 #[clap(long_help = r#"
1267Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
1268
1269Examples:
1270 # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
1271 --pcie-switch rp0:switch0,num_downstream_ports=4
1272
1273 # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
1274 --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
1275
1276 # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
1277 --pcie-switch rp0:switch0
1278 --pcie-switch switch0-downstream-0:switch1
1279 --pcie-switch switch1-downstream-1:switch2
1280
1281 # Enable hotplug on all downstream switch ports of switch0
1282 --pcie-switch rp0:switch0,hotplug
1283
1284 # Enable PASID on all downstream switch ports of switch0
1285 --pcie-switch rp0:switch0,pasid
1286
1287Syntax: <port_name>:<name>[,opt,opt=arg,...]
1288
1289 port_name can be:
1290 - Root port name (e.g., "rp0") to connect directly to a root port
1291 - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
1292
1293Options:
1294 `hotplug` enable hotplug support for all downstream switch ports
1295 `num_downstream_ports=<value>` number of downstream ports, default 4
1296 `acs=<mask>` ACS capability bitmask for downstream switch ports
1297 `pasid` configure this port to support PASID for downstream devices
1298"#)]
1299 #[clap(long, conflicts_with("pcat"))]
1300 pub pcie_switch: Vec<GenericPcieSwitchCli>,
1301
1302 #[clap(long_help = r#"
1304Declare that the device directly behind a PCIe port is a generic initiator
1305(GI) for a NUMA node, generating an SRAT Generic Initiator Affinity structure.
1306
1307The port may be a root port or a switch downstream port, so this works for
1308devices that sit behind a switch (e.g. a GPU placed under a switch shared
1309with a NIC for peer-to-peer DMA). The port is resolved by name against the
1310live topology after switch downstream ports are enumerated.
1311
1312Examples:
1313 # The device behind switch downstream port sw1-downstream-0 is a generic
1314 # initiator for NUMA node 1
1315 --pcie-generic-initiator port=sw1-downstream-0,node=1
1316
1317 # Also works for a root port name
1318 --pcie-generic-initiator port=rp0,node=2
1319
1320Syntax: port=<port_name>,node=<node>
1321"#)]
1322 #[clap(
1323 long = "pcie-generic-initiator",
1324 value_name = "port=<name>,node=<node>",
1325 conflicts_with("pcat")
1326 )]
1327 pub pcie_generic_initiator: Vec<PcieGenericInitiatorCli>,
1328
1329 #[clap(long_help = r#"
1331Attach PCIe devices to root ports or downstream switch ports
1332which are implemented in a simulator running in a remote process.
1333
1334Examples:
1335 # Attach to root port rc0rp0 with default socket
1336 --pcie-remote rc0rp0
1337
1338 # Attach with custom socket address
1339 --pcie-remote rc0rp0,socket=0.0.0.0:48914
1340
1341 # Specify HU and controller identifiers
1342 --pcie-remote rc0rp0,hu=1,controller=0
1343
1344 # Multiple devices on different ports
1345 --pcie-remote rc0rp0,socket=0.0.0.0:48914
1346 --pcie-remote rc0rp1,socket=0.0.0.0:48915
1347
1348Syntax: <port_name>[,opt=arg,...]
1349
1350Options:
1351 `socket=<address>` TCP socket (default: localhost:48914)
1352 `hu=<value>` Hardware unit identifier (default: 0)
1353 `controller=<value>` Controller identifier (default: 0)
1354"#)]
1355 #[clap(long, conflicts_with("pcat"))]
1356 pub pcie_remote: Vec<PcieRemoteCli>,
1357
1358 #[clap(long_help = r#"
1360Assign a host PCI device to the guest via Linux VFIO.
1361
1362The device must be bound to vfio-pci on the host before starting the VM.
1363
1364Examples:
1365 --vfio host=0000:01:00.0,port=rp0
1366 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1367
1368Keys:
1369 host=<pci_bdf> (required) PCI address on the host
1370 port=<name> (required) Root port or downstream switch port name
1371 iommu=<id> (optional) Reference to an --iommu object. When present,
1372 uses VFIO cdev + iommufd instead of the legacy group path.
1373"#)]
1374 #[cfg(target_os = "linux")]
1375 #[clap(long, conflicts_with("pcat"))]
1376 pub vfio: Vec<VfioDeviceCli>,
1377
1378 #[clap(long_help = r#"
1380Declare an iommufd context. Opens /dev/iommu so it can be referenced by
1381--vfio devices via the iommu=<id> key. The associated IOAS is allocated
1382the first time a --vfio device referring to this id is opened.
1383
1384Requires Linux kernel >= 6.6 with iommufd support.
1385
1386Examples:
1387 --iommu id=iommu0 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1388
1389Syntax: id=<name>
1390"#)]
1391 #[cfg(target_os = "linux")]
1392 #[clap(long, conflicts_with("pcat"))]
1393 pub iommu: Vec<IommuCli>,
1394}
1395
1396impl Options {
1397 pub fn memory_size(&self) -> u64 {
1399 self.memory.size.map(|m| m.0).unwrap_or(DEFAULT_MEMORY_SIZE)
1400 }
1401
1402 pub fn prefetch_memory(&self) -> bool {
1404 self.memory.prefetch || self.deprecated_prefetch
1405 }
1406
1407 pub fn private_memory(&self) -> bool {
1409 self.memory.shared == Some(false) || self.deprecated_private_memory
1410 }
1411
1412 pub fn transparent_hugepages(&self) -> bool {
1414 self.memory
1415 .transparent_hugepages
1416 .unwrap_or(!self.memory.hugepages)
1417 || self.deprecated_thp
1418 }
1419
1420 pub fn memory_backing_file(&self) -> Option<&PathBuf> {
1422 self.memory
1423 .file
1424 .as_ref()
1425 .or(self.deprecated_memory_backing_file.as_ref())
1426 }
1427
1428 pub fn validate_memory_options(&self) -> anyhow::Result<()> {
1436 if self.memory.file.is_some() && self.deprecated_memory_backing_file.is_some() {
1437 anyhow::bail!("--memory file=... conflicts with --memory-backing-file");
1438 }
1439 if self.memory.file.is_some() && self.restore_snapshot.is_some() {
1440 anyhow::bail!("--memory file=... conflicts with --restore-snapshot");
1441 }
1442 if self.memory.shared == Some(true) && self.deprecated_private_memory {
1443 anyhow::bail!("--memory shared=on conflicts with --private-memory");
1444 }
1445 Ok(())
1446 }
1447
1448 pub fn validate_isolation_options(&self) -> anyhow::Result<()> {
1450 if self.snp_restricted_injection && !matches!(self.isolation, Some(IsolationCli::Snp)) {
1451 anyhow::bail!("--snp-restricted-injection requires --isolation snp");
1452 }
1453 if self.snp_restricted_injection
1454 && self
1455 .hypervisor
1456 .as_deref()
1457 .and_then(|value| value.split(':').next())
1458 != Some("mshv")
1459 {
1460 anyhow::bail!("--snp-restricted-injection requires --hypervisor mshv");
1461 }
1462 if self.snp_restricted_injection
1463 && (self.uefi || self.pcat || self.igvm.is_some() || self.restore_snapshot.is_some())
1464 {
1465 anyhow::bail!("--snp-restricted-injection requires Linux direct boot");
1466 }
1467 if matches!(self.isolation, Some(IsolationCli::Snp)) {
1468 if self.uefi {
1469 anyhow::bail!("SNP isolation currently only supports Linux direct boot");
1470 }
1471 if self.memory.hugepages
1472 || self
1473 .numa
1474 .as_ref()
1475 .is_some_and(|nodes| nodes.iter().any(|node| node.memory.hugepages))
1476 {
1477 anyhow::bail!("SNP isolation currently does not support hugetlb memory");
1478 }
1479 }
1480 Ok(())
1481 }
1482
1483 pub fn validate_igvm_options(&self) -> anyhow::Result<()> {
1485 if self.igvm.is_some() && !self.vtl2 && self.igvm_personality.is_none() {
1486 anyhow::bail!("--igvm-personality is required for non-VTL2 IGVM boots");
1487 }
1488 Ok(())
1489 }
1490}
1491
1492#[derive(Clone, Debug, PartialEq)]
1493pub struct FsArgs {
1494 pub tag: String,
1495 pub path: String,
1496 pub pcie_port: Option<String>,
1497}
1498
1499impl FromStr for FsArgs {
1500 type Err = anyhow::Error;
1501
1502 fn from_str(s: &str) -> Result<Self, Self::Err> {
1503 let (pcie_port, s) = parse_pcie_port_prefix(s);
1504 let mut s = s.split(',');
1505 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
1506 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
1507 };
1508 Ok(Self {
1509 tag: tag.to_owned(),
1510 path: path.to_owned(),
1511 pcie_port,
1512 })
1513 }
1514}
1515
1516#[derive(Clone, Debug, PartialEq)]
1517pub struct FsArgsWithOptions {
1518 pub tag: String,
1520 pub path: String,
1522 pub options: String,
1524 pub pcie_port: Option<String>,
1526}
1527
1528impl FromStr for FsArgsWithOptions {
1529 type Err = anyhow::Error;
1530
1531 fn from_str(s: &str) -> Result<Self, Self::Err> {
1532 let (pcie_port, s) = parse_pcie_port_prefix(s);
1533 let mut s = s.split(',');
1534 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
1535 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
1536 };
1537 let options = s.collect::<Vec<_>>().join(";");
1538 Ok(Self {
1539 tag: tag.to_owned(),
1540 path: path.to_owned(),
1541 options,
1542 pcie_port,
1543 })
1544 }
1545}
1546
1547#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1551pub enum GuestPowerAction {
1552 Reset,
1554 Halt,
1557 Exit(u8),
1559}
1560
1561fn parse_guest_power_action(s: &str) -> Result<GuestPowerAction, String> {
1564 match s {
1565 "reset" => Ok(GuestPowerAction::Reset),
1566 "halt" => Ok(GuestPowerAction::Halt),
1567 "exit" => Ok(GuestPowerAction::Exit(0)),
1568 _ => match s.strip_prefix("exit:") {
1569 Some(code) => code
1570 .parse::<u8>()
1571 .map(GuestPowerAction::Exit)
1572 .map_err(|err| format!("invalid exit code '{code}' (expected 0-255): {err}")),
1573 None => Err(format!(
1574 "expected reset, halt, exit, or exit:<code>, got '{s}'"
1575 )),
1576 },
1577 }
1578}
1579
1580#[derive(Copy, Clone, clap::ValueEnum)]
1581pub enum VirtioBusCli {
1582 Auto,
1583 Mmio,
1584 Pci,
1585 Vpci,
1586}
1587
1588fn parse_virtio_vsock_bus(value: &str) -> Result<VirtioBusCli, String> {
1589 match VirtioBusCli::from_str(value, true) {
1590 Ok(bus @ (VirtioBusCli::Mmio | VirtioBusCli::Pci)) => Ok(bus),
1591 _ => Err("expected mmio or pci".to_string()),
1592 }
1593}
1594
1595#[cfg(target_os = "linux")]
1596fn parse_vhost_vsock_cid(value: &str) -> Result<u32, String> {
1597 let cid = value
1598 .parse::<u32>()
1599 .map_err(|error| format!("invalid CID '{value}': {error}"))?;
1600 if !(3..u32::MAX).contains(&cid) {
1601 return Err(format!("CID must be between 3 and {}", u32::MAX - 1));
1602 }
1603 Ok(cid)
1604}
1605
1606fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
1611 if let Some(rest) = s.strip_prefix("pcie_port=") {
1612 if let Some((port, rest)) = rest.split_once(':') {
1613 if !port.is_empty() {
1614 return (Some(port.to_string()), rest);
1615 }
1616 }
1617 }
1618 (None, s)
1619}
1620
1621#[derive(Clone, Debug, PartialEq)]
1622pub struct VirtioPmemArgs {
1623 pub path: String,
1624 pub pcie_port: Option<String>,
1625}
1626
1627impl FromStr for VirtioPmemArgs {
1628 type Err = anyhow::Error;
1629
1630 fn from_str(s: &str) -> Result<Self, Self::Err> {
1631 let (pcie_port, s) = parse_pcie_port_prefix(s);
1632 if s.is_empty() {
1633 anyhow::bail!("expected [pcie_port=<port>:]<path>");
1634 }
1635 Ok(Self {
1636 path: s.to_owned(),
1637 pcie_port,
1638 })
1639 }
1640}
1641
1642#[derive(clap::ValueEnum, Clone, Copy)]
1643pub enum SecureBootTemplateCli {
1644 Windows,
1645 UefiCa,
1646}
1647
1648#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
1650pub enum TpmVersionCli {
1651 #[value(name = "138", alias = "1.38")]
1653 V138,
1654 #[value(name = "185", alias = "1.85")]
1656 V185,
1657}
1658
1659fn parse_memory(s: &str) -> anyhow::Result<u64> {
1660 if s == "VMGS_DEFAULT" {
1661 Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
1662 } else {
1663 || -> Option<u64> {
1664 let mut b = s.as_bytes();
1665 if s.ends_with('B') {
1666 b = &b[..b.len() - 1]
1667 }
1668 if b.is_empty() {
1669 return None;
1670 }
1671 let multi = match b[b.len() - 1] as char {
1672 'T' => Some(1024 * 1024 * 1024 * 1024),
1673 'G' => Some(1024 * 1024 * 1024),
1674 'M' => Some(1024 * 1024),
1675 'K' => Some(1024),
1676 _ => None,
1677 };
1678 if multi.is_some() {
1679 b = &b[..b.len() - 1]
1680 }
1681 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
1682 n.checked_mul(multi.unwrap_or(1))
1683 }()
1684 .with_context(|| format!("invalid memory size '{0}'", s))
1685 }
1686}
1687
1688fn parse_address(s: &str) -> anyhow::Result<u64> {
1690 let hex = s
1691 .strip_prefix("0x")
1692 .or_else(|| s.strip_prefix("0X"))
1693 .with_context(|| format!("invalid address '{s}', expected a 0x-prefixed hex value"))?;
1694 u64::from_str_radix(hex, 16).with_context(|| format!("invalid address '{s}'"))
1695}
1696
1697fn parse_acs_capability_mask(value: &str) -> anyhow::Result<u16> {
1698 if let Some(hex) = value
1699 .strip_prefix("0x")
1700 .or_else(|| value.strip_prefix("0X"))
1701 {
1702 u16::from_str_radix(hex, 16).context("invalid ACS capability mask")
1703 } else {
1704 value.parse::<u16>().context("invalid ACS capability mask")
1705 }
1706}
1707
1708#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1711pub struct SmbiosRelease(pub u8, pub u8);
1712
1713impl FromStr for SmbiosRelease {
1714 type Err = anyhow::Error;
1715
1716 fn from_str(value: &str) -> anyhow::Result<Self> {
1717 let (major, minor) = value
1718 .split_once('.')
1719 .with_context(|| format!("invalid smbios release '{value}', expected MAJOR.MINOR"))?;
1720 let major = major
1721 .parse::<u8>()
1722 .with_context(|| format!("invalid smbios release major '{major}'"))?;
1723 let minor = minor
1724 .parse::<u8>()
1725 .with_context(|| format!("invalid smbios release minor '{minor}'"))?;
1726 Ok(SmbiosRelease(major, minor))
1727 }
1728}
1729
1730#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1733pub enum SmbiosUuid {
1734 Random,
1736 Fixed(Guid),
1738}
1739
1740impl FromStr for SmbiosUuid {
1741 type Err = anyhow::Error;
1742
1743 fn from_str(value: &str) -> anyhow::Result<Self> {
1744 if value == "random" {
1745 Ok(SmbiosUuid::Random)
1746 } else {
1747 let guid = value
1748 .parse::<Guid>()
1749 .with_context(|| format!("invalid smbios uuid '{value}'"))?;
1750 Ok(SmbiosUuid::Fixed(guid))
1751 }
1752 }
1753}
1754
1755#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
1758pub struct SmbiosBiosCli {
1759 pub vendor: Option<String>,
1761 pub version: Option<String>,
1763 #[kv(key = "date")]
1765 pub release_date: Option<String>,
1766 pub release: Option<SmbiosRelease>,
1768}
1769
1770#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
1773pub struct SmbiosSystemCli {
1774 pub manufacturer: Option<String>,
1776 #[kv(key = "product")]
1778 pub product_name: Option<String>,
1779 pub version: Option<String>,
1781 #[kv(key = "serial")]
1783 pub serial_number: Option<String>,
1784 #[kv(key = "sku")]
1786 pub sku_number: Option<String>,
1787 pub family: Option<String>,
1789 pub uuid: Option<SmbiosUuid>,
1791}
1792
1793#[derive(Debug, Clone, Default, PartialEq, Eq)]
1800pub struct SmbiosCli {
1801 pub bios: SmbiosBiosCli,
1803 pub system: SmbiosSystemCli,
1805}
1806
1807fn smbios_set_once<T>(slot: &mut Option<T>, key: &str, value: T) -> anyhow::Result<()> {
1809 anyhow::ensure!(slot.is_none(), "duplicate smbios option '{key}'");
1810 *slot = Some(value);
1811 Ok(())
1812}
1813
1814impl SmbiosCli {
1815 pub fn merge(&mut self, other: SmbiosCli) -> anyhow::Result<()> {
1818 let SmbiosCli {
1819 bios:
1820 SmbiosBiosCli {
1821 vendor,
1822 version: bios_version,
1823 release_date,
1824 release,
1825 },
1826 system:
1827 SmbiosSystemCli {
1828 manufacturer,
1829 product_name,
1830 version: system_version,
1831 serial_number,
1832 sku_number,
1833 family,
1834 uuid,
1835 },
1836 } = other;
1837 merge_smbios_field(&mut self.bios.vendor, vendor, 0, "vendor")?;
1838 merge_smbios_field(&mut self.bios.version, bios_version, 0, "version")?;
1839 merge_smbios_field(&mut self.bios.release_date, release_date, 0, "date")?;
1840 merge_smbios_field(&mut self.bios.release, release, 0, "release")?;
1841 merge_smbios_field(
1842 &mut self.system.manufacturer,
1843 manufacturer,
1844 1,
1845 "manufacturer",
1846 )?;
1847 merge_smbios_field(&mut self.system.product_name, product_name, 1, "product")?;
1848 merge_smbios_field(&mut self.system.version, system_version, 1, "version")?;
1849 merge_smbios_field(&mut self.system.serial_number, serial_number, 1, "serial")?;
1850 merge_smbios_field(&mut self.system.sku_number, sku_number, 1, "sku")?;
1851 merge_smbios_field(&mut self.system.family, family, 1, "family")?;
1852 merge_smbios_field(&mut self.system.uuid, uuid, 1, "uuid")?;
1853 Ok(())
1854 }
1855}
1856
1857fn merge_smbios_field<T>(
1860 dst: &mut Option<T>,
1861 src: Option<T>,
1862 typ: u8,
1863 key: &str,
1864) -> anyhow::Result<()> {
1865 if let Some(value) = src {
1866 smbios_set_once(dst, &format!("type={typ} {key}"), value)?;
1867 }
1868 Ok(())
1869}
1870
1871fn parse_smbios(s: &str) -> anyhow::Result<SmbiosCli> {
1878 let mut typ: Option<u8> = None;
1879 let mut rest = Vec::new();
1880 for part in s.split(',') {
1881 let (key, value) = part
1882 .split_once('=')
1883 .with_context(|| format!("invalid smbios option '{part}', expected key=value"))?;
1884 if key.is_empty() || value.is_empty() {
1885 anyhow::bail!("invalid smbios option '{part}', expected key=value");
1886 }
1887 if key == "type" {
1888 anyhow::ensure!(typ.is_none(), "duplicate smbios option 'type'");
1889 typ = Some(
1890 value
1891 .parse::<u8>()
1892 .with_context(|| format!("invalid smbios type '{value}'"))?,
1893 );
1894 } else {
1895 rest.push(part);
1896 }
1897 }
1898
1899 let typ = typ.context("smbios option requires 'type=N' (e.g. 'type=1')")?;
1900 let rest = rest.join(",");
1901 Ok(match typ {
1902 0 => SmbiosCli {
1903 bios: rest.parse()?,
1904 ..Default::default()
1905 },
1906 1 => SmbiosCli {
1907 system: rest.parse()?,
1908 ..Default::default()
1909 },
1910 other => anyhow::bail!("unsupported smbios type '{other}' (expected 0 or 1)"),
1911 })
1912}
1913
1914fn parse_memory_config(s: &str) -> anyhow::Result<MemoryCli> {
1915 let memory = if !s.contains('=') && !s.contains(',') {
1917 MemoryCli {
1918 size: Some(s.parse::<vmm_cli::MemorySize>()?),
1919 ..Default::default()
1920 }
1921 } else {
1922 s.parse::<MemoryCli>()?
1923 };
1924 memory.validate()?;
1925 Ok(memory)
1926}
1927
1928fn parse_numa_node(s: &str) -> anyhow::Result<NumaNodeCli> {
1929 let node: NumaNodeCli = s.parse()?;
1930 anyhow::ensure!(
1931 node.memory.size.is_some(),
1932 "numa node requires 'size' option"
1933 );
1934 anyhow::ensure!(
1935 node.memory.file.is_none(),
1936 "'file' is not supported in --numa"
1937 );
1938 node.memory.validate()?;
1939 Ok(node)
1940}
1941
1942fn parse_numa_distance(s: &str) -> anyhow::Result<NumaDistanceCli> {
1943 let parts: Vec<&str> = s.split(':').collect();
1944 anyhow::ensure!(
1945 parts.len() == 3,
1946 "expected SRC:DST:DISTANCE format, got '{s}'"
1947 );
1948 let src = parts[0].parse::<u32>().context("invalid source node")?;
1949 let dst = parts[1]
1950 .parse::<u32>()
1951 .context("invalid destination node")?;
1952 let distance = parts[2].parse::<u8>().context("invalid distance")?;
1953 anyhow::ensure!(
1954 distance >= 10,
1955 "distance must be >= 10 (10 = local), got {distance}"
1956 );
1957 Ok(NumaDistanceCli { src, dst, distance })
1958}
1959
1960fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
1962 match s.strip_prefix("0x") {
1963 Some(rest) => u64::from_str_radix(rest, 16),
1964 None => s.parse::<u64>(),
1965 }
1966}
1967
1968#[derive(Clone, Debug, PartialEq)]
1969pub enum DiskCliKind {
1970 Memory(u64),
1972 MemoryDiff(Box<DiskCliKind>),
1974 Sqlite {
1976 path: PathBuf,
1977 create_with_len: Option<u64>,
1978 },
1979 SqliteDiff {
1981 path: PathBuf,
1982 create: bool,
1983 disk: Box<DiskCliKind>,
1984 },
1985 AutoCacheSqlite {
1987 cache_path: String,
1988 key: Option<String>,
1989 disk: Box<DiskCliKind>,
1990 },
1991 PersistentReservationsWrapper(Box<DiskCliKind>),
1993 File {
1995 path: PathBuf,
1996 create_with_len: Option<u64>,
1997 direct: bool,
1998 },
1999 Blob {
2001 kind: BlobKind,
2002 url: String,
2003 },
2004 Crypt {
2006 cipher: DiskCipher,
2007 key_file: PathBuf,
2008 disk: Box<DiskCliKind>,
2009 },
2010 DelayDiskWrapper {
2012 delay_ms: u64,
2013 disk: Box<DiskCliKind>,
2014 },
2015}
2016
2017#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
2018pub enum DiskCipher {
2019 #[clap(name = "xts-aes-256")]
2020 XtsAes256,
2021}
2022
2023#[derive(Copy, Clone, Debug, PartialEq)]
2024pub enum BlobKind {
2025 Flat,
2026 Vhd1,
2027}
2028
2029struct FileOpts {
2030 path: PathBuf,
2031 create_with_len: Option<u64>,
2032 direct: bool,
2033}
2034
2035fn parse_file_opts(arg: &str) -> anyhow::Result<FileOpts> {
2036 let mut path = arg;
2037 let mut create_with_len = None;
2038 let mut direct = false;
2039
2040 if let Some((p, rest)) = arg.split_once(';') {
2042 path = p;
2043 for opt in rest.split(';') {
2044 if let Some(len) = opt.strip_prefix("create=") {
2045 create_with_len = Some(parse_memory(len)?);
2046 } else if opt == "direct" {
2047 direct = true;
2048 } else {
2049 anyhow::bail!("invalid file option '{opt}', expected 'create=<len>' or 'direct'");
2050 }
2051 }
2052 }
2053
2054 Ok(FileOpts {
2055 path: path.into(),
2056 create_with_len,
2057 direct,
2058 })
2059}
2060
2061impl DiskCliKind {
2062 fn parse_autocache(
2065 arg: &str,
2066 cache_path: Result<String, std::env::VarError>,
2067 ) -> anyhow::Result<Self> {
2068 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
2069 let cache_path = cache_path.context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
2070 Ok(DiskCliKind::AutoCacheSqlite {
2071 cache_path,
2072 key: (!key.is_empty()).then(|| key.to_string()),
2073 disk: Box::new(kind.parse()?),
2074 })
2075 }
2076}
2077
2078impl FromStr for DiskCliKind {
2079 type Err = anyhow::Error;
2080
2081 fn from_str(s: &str) -> anyhow::Result<Self> {
2082 let disk = match s.split_once(':') {
2083 None => {
2085 let FileOpts {
2086 path,
2087 create_with_len,
2088 direct,
2089 } = parse_file_opts(s)?;
2090 DiskCliKind::File {
2091 path,
2092 create_with_len,
2093 direct,
2094 }
2095 }
2096 Some((kind, arg)) => match kind {
2097 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
2098 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
2099 "sql" => {
2100 let FileOpts {
2101 path,
2102 create_with_len,
2103 direct,
2104 } = parse_file_opts(arg)?;
2105 if direct {
2106 anyhow::bail!("'direct' is not supported for 'sql' disks");
2107 }
2108 DiskCliKind::Sqlite {
2109 path,
2110 create_with_len,
2111 }
2112 }
2113 "sqldiff" => {
2114 let (path_and_opts, kind) =
2115 arg.split_once(':').context("expected path[;opts]:kind")?;
2116 let disk = Box::new(kind.parse()?);
2117 match path_and_opts.split_once(';') {
2118 Some((path, create)) => {
2119 if create != "create" {
2120 anyhow::bail!("invalid syntax after ';', expected 'create'")
2121 }
2122 DiskCliKind::SqliteDiff {
2123 path: path.into(),
2124 create: true,
2125 disk,
2126 }
2127 }
2128 None => DiskCliKind::SqliteDiff {
2129 path: path_and_opts.into(),
2130 create: false,
2131 disk,
2132 },
2133 }
2134 }
2135 "autocache" => {
2136 Self::parse_autocache(arg, std::env::var("OPENVMM_AUTO_CACHE_PATH"))?
2137 }
2138 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
2139 "file" => {
2140 let FileOpts {
2141 path,
2142 create_with_len,
2143 direct,
2144 } = parse_file_opts(arg)?;
2145 DiskCliKind::File {
2146 path,
2147 create_with_len,
2148 direct,
2149 }
2150 }
2151 "blob" => {
2152 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
2153 let blob_kind = match blob_kind {
2154 "flat" => BlobKind::Flat,
2155 "vhd1" => BlobKind::Vhd1,
2156 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
2157 };
2158 DiskCliKind::Blob {
2159 kind: blob_kind,
2160 url: url.to_string(),
2161 }
2162 }
2163 "crypt" => {
2164 let (cipher, (key, kind)) = arg
2165 .split_once(':')
2166 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
2167 .context("expected cipher:key_file:kind")?;
2168 DiskCliKind::Crypt {
2169 cipher: ValueEnum::from_str(cipher, false)
2170 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
2171 key_file: PathBuf::from(key),
2172 disk: Box::new(kind.parse()?),
2173 }
2174 }
2175 kind => {
2176 let FileOpts {
2181 path,
2182 create_with_len,
2183 direct,
2184 } = parse_file_opts(s)?;
2185 if path.has_root() {
2186 DiskCliKind::File {
2187 path,
2188 create_with_len,
2189 direct,
2190 }
2191 } else {
2192 anyhow::bail!("invalid disk kind {kind}");
2193 }
2194 }
2195 },
2196 };
2197 Ok(disk)
2198 }
2199}
2200
2201#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2203pub enum RpcTransportCli {
2204 #[default]
2207 Auto,
2208 Ttrpc,
2210 Grpc,
2212}
2213
2214#[derive(Clone, Debug, PartialEq, Eq)]
2216pub struct RpcCli {
2217 pub path: PathBuf,
2219 pub transport: RpcTransportCli,
2221}
2222
2223impl FromStr for RpcCli {
2224 type Err = anyhow::Error;
2225
2226 fn from_str(s: &str) -> anyhow::Result<Self> {
2227 let mut path = None;
2228 let mut transport = None;
2229 for part in s.split(',') {
2230 let (key, value) = part
2231 .split_once('=')
2232 .with_context(|| format!("invalid rpc option '{part}', expected key=value"))?;
2233 match key {
2234 "path" => {
2235 anyhow::ensure!(path.is_none(), "duplicate option 'path'");
2236 anyhow::ensure!(!value.is_empty(), "'path' requires a value");
2237 path = Some(PathBuf::from(value));
2238 }
2239 "transport" => {
2240 anyhow::ensure!(transport.is_none(), "duplicate option 'transport'");
2241 transport = Some(match value {
2242 "auto" => RpcTransportCli::Auto,
2243 "ttrpc" => RpcTransportCli::Ttrpc,
2244 "grpc" => RpcTransportCli::Grpc,
2245 _ => anyhow::bail!(
2246 "invalid transport '{value}', expected auto, ttrpc, or grpc"
2247 ),
2248 });
2249 }
2250 _ => anyhow::bail!("unknown rpc option '{key}'"),
2251 }
2252 }
2253
2254 Ok(RpcCli {
2255 path: path.context("'path' is required")?,
2256 transport: transport.unwrap_or_default(),
2257 })
2258 }
2259}
2260
2261#[derive(Clone)]
2262pub struct VmgsCli {
2263 pub kind: DiskCliKind,
2264 pub provision: ProvisionVmgs,
2265}
2266
2267#[derive(Copy, Clone)]
2268pub enum ProvisionVmgs {
2269 OnEmpty,
2270 OnFailure,
2271 True,
2272}
2273
2274impl FromStr for VmgsCli {
2275 type Err = anyhow::Error;
2276
2277 fn from_str(s: &str) -> anyhow::Result<Self> {
2278 let (kind, opt) = s
2279 .split_once(',')
2280 .map(|(k, o)| (k, Some(o)))
2281 .unwrap_or((s, None));
2282 let kind = kind.parse()?;
2283
2284 let provision = match opt {
2285 None => ProvisionVmgs::OnEmpty,
2286 Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
2287 Some("fmt") => ProvisionVmgs::True,
2288 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
2289 };
2290
2291 Ok(VmgsCli { kind, provision })
2292 }
2293}
2294
2295#[derive(clap::Args)]
2297pub struct VncCli {
2298 #[clap(long)]
2300 pub vnc: bool,
2301
2302 #[clap(long, value_name = "PORT", default_value = "5900")]
2304 pub vnc_port: u16,
2305
2306 #[clap(long, value_name = "ADDRESS", default_value = "127.0.0.1")]
2310 pub vnc_listen: String,
2311
2312 #[clap(long, value_name = "COUNT", default_value = "16")]
2314 pub vnc_max_clients: usize,
2315
2316 #[clap(long)]
2319 pub vnc_evict_oldest: bool,
2320}
2321
2322#[derive(Clone)]
2324pub struct DiskCli {
2325 pub vtl: DeviceVtl,
2326 pub kind: DiskCliKind,
2327 pub read_only: bool,
2328 pub is_dvd: bool,
2329 pub underhill: Option<UnderhillDiskSource>,
2330 pub pcie_port: Option<String>,
2331 pub controller: Option<String>,
2332 pub nsid: Option<u32>,
2333 pub lun: Option<u8>,
2334 pub relay: Option<(String, Option<u32>)>,
2335}
2336
2337#[derive(Copy, Clone)]
2338pub enum UnderhillDiskSource {
2339 Scsi,
2340 Nvme,
2341}
2342
2343struct RelayTarget {
2345 name: String,
2346 location: Option<u32>,
2347}
2348
2349impl FromStr for RelayTarget {
2350 type Err = anyhow::Error;
2351
2352 fn from_str(s: &str) -> anyhow::Result<Self> {
2353 Ok(if let Some((name, loc)) = s.split_once(':') {
2354 RelayTarget {
2355 name: name.to_string(),
2356 location: Some(loc.parse::<u32>().context("invalid relay location")?),
2357 }
2358 } else {
2359 RelayTarget {
2360 name: s.to_string(),
2361 location: None,
2362 }
2363 })
2364 }
2365}
2366
2367#[derive(vmm_cli::KeyValueArgs)]
2370struct DiskArgs {
2371 #[kv(positional)]
2372 kind: DiskCliKind,
2373 #[kv(flag)]
2374 ro: bool,
2375 #[kv(flag)]
2376 dvd: bool,
2377 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2378 vtl: DeviceVtl,
2379 #[kv(flag)]
2380 uh: bool,
2381 #[kv(flag, key = "uh-nvme")]
2382 uh_nvme: bool,
2383 pcie_port: Option<String>,
2384 #[kv(key = "on")]
2385 controller: Option<String>,
2386 nsid: Option<u32>,
2387 lun: Option<u8>,
2388 relay: Option<RelayTarget>,
2389}
2390
2391impl FromStr for DiskCli {
2392 type Err = anyhow::Error;
2393
2394 fn from_str(s: &str) -> anyhow::Result<Self> {
2395 let args: DiskArgs = s.parse()?;
2396
2397 let underhill = match (args.uh, args.uh_nvme) {
2398 (false, false) => None,
2399 (true, false) => Some(UnderhillDiskSource::Scsi),
2400 (false, true) => Some(UnderhillDiskSource::Nvme),
2401 (true, true) => anyhow::bail!("`uh` and `uh-nvme` are mutually exclusive"),
2402 };
2403 let read_only = args.ro || args.dvd;
2404 let is_dvd = args.dvd;
2405 let vtl = args.vtl;
2406 let pcie_port = args.pcie_port;
2407 let controller = args.controller;
2408 let nsid = args.nsid;
2409 let lun = args.lun;
2410 let relay = args.relay.map(|r| (r.name, r.location));
2411
2412 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
2413 anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
2414 }
2415
2416 if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
2417 anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
2418 }
2419
2420 if controller.is_some() && pcie_port.is_some() {
2421 anyhow::bail!("`on` is incompatible with `pcie_port`");
2422 }
2423
2424 if controller.is_some() && vtl != DeviceVtl::Vtl0 {
2425 anyhow::bail!(
2426 "`vtl2` is incompatible with `on`; the controller's VTL determines placement"
2427 );
2428 }
2429
2430 if controller.is_some() && underhill.is_some() {
2431 anyhow::bail!("`on` is incompatible with `uh` and `uh-nvme`; use `relay` instead");
2432 }
2433
2434 if nsid.is_some() && controller.is_none() {
2435 anyhow::bail!("`nsid` requires `on`");
2436 }
2437
2438 if lun.is_some() && controller.is_none() {
2439 anyhow::bail!("`lun` requires `on`");
2440 }
2441
2442 if nsid.is_some() && lun.is_some() {
2443 anyhow::bail!("`nsid` and `lun` are mutually exclusive");
2444 }
2445
2446 if relay.is_some() && controller.is_none() {
2447 anyhow::bail!("`relay` requires `on`");
2448 }
2449
2450 if relay.is_some() && underhill.is_some() {
2451 anyhow::bail!("`relay` is incompatible with `uh` and `uh-nvme`");
2452 }
2453
2454 Ok(DiskCli {
2455 vtl,
2456 kind: args.kind,
2457 read_only,
2458 is_dvd,
2459 underhill,
2460 pcie_port,
2461 controller,
2462 nsid,
2463 lun,
2464 relay,
2465 })
2466 }
2467}
2468
2469#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueGroup)]
2471pub enum NvmeControllerTransport {
2472 #[kv(key = "pcie_port")]
2474 Pcie(String),
2475 #[kv(key = "vpci")]
2477 Vpci(Option<Guid>),
2478}
2479
2480#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2482pub struct NvmeControllerCli {
2483 pub id: String,
2485 #[kv(flatten)]
2487 pub transport: NvmeControllerTransport,
2488 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2490 pub vtl: DeviceVtl,
2491}
2492
2493#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2495pub struct ScsiControllerCli {
2496 pub id: String,
2498 #[kv(default)]
2500 pub sub_channels: u16,
2501 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2503 pub vtl: DeviceVtl,
2504}
2505
2506#[derive(Copy, Clone, Debug, PartialEq)]
2508pub enum OpenhclControllerType {
2509 Scsi,
2510 Nvme,
2511}
2512
2513#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2515pub struct OpenhclControllerCli {
2516 pub id: String,
2518 #[kv(key = "type")]
2520 pub controller_type: OpenhclControllerType,
2521 pub guid: Option<Guid>,
2523}
2524
2525impl FromStr for OpenhclControllerType {
2526 type Err = anyhow::Error;
2527
2528 fn from_str(s: &str) -> anyhow::Result<Self> {
2529 Ok(match s {
2530 "scsi" => OpenhclControllerType::Scsi,
2531 "nvme" => OpenhclControllerType::Nvme,
2532 other => anyhow::bail!("unknown controller type: '{other}'"),
2533 })
2534 }
2535}
2536
2537#[derive(Clone, Debug, PartialEq)]
2539pub struct CxlTestDeviceCli {
2540 pub hdm_size: u64,
2542 pub pcie_port: String,
2544}
2545
2546impl FromStr for CxlTestDeviceCli {
2547 type Err = anyhow::Error;
2548
2549 fn from_str(s: &str) -> anyhow::Result<Self> {
2550 let mut opts = s.split(',');
2551 let first = opts.next().context("expected CXL test device config")?;
2552 let (kind, arg) = first
2553 .split_once(':')
2554 .context("expected CXL test syntax: mem:<len>")?;
2555
2556 if kind != "mem" {
2557 anyhow::bail!("unsupported CXL test backing kind '{kind}', expected 'mem'");
2558 }
2559
2560 let hdm_size = parse_memory(arg).context("failed to parse CXL test HDM size")?;
2561 let mut pcie_port = None;
2562
2563 for opt in opts {
2564 let mut kv = opt.split('=');
2565 let key = kv.next().unwrap_or_default();
2566 match key {
2567 "pcie_port" => {
2568 let val = kv.next();
2569 if val.is_none_or(|v| v.is_empty()) {
2570 anyhow::bail!("`pcie_port` requires a port name");
2571 }
2572 pcie_port = Some(val.unwrap().to_string());
2573 }
2574 _ => anyhow::bail!("unknown option: '{opt}'"),
2575 }
2576 }
2577
2578 let Some(pcie_port) = pcie_port else {
2579 anyhow::bail!("`pcie_port=<name>` is required for `--cxl-test`");
2580 };
2581
2582 Ok(Self {
2583 hdm_size,
2584 pcie_port,
2585 })
2586 }
2587}
2588
2589#[derive(Clone)]
2591pub struct IdeDiskCli {
2592 pub kind: DiskCliKind,
2593 pub read_only: bool,
2594 pub channel: Option<u8>,
2595 pub device: Option<u8>,
2596 pub is_dvd: bool,
2597}
2598
2599impl FromStr for IdeDiskCli {
2600 type Err = anyhow::Error;
2601
2602 fn from_str(s: &str) -> anyhow::Result<Self> {
2603 let mut opts = s.split(',');
2604 let kind = opts.next().unwrap().parse()?;
2605
2606 let mut read_only = false;
2607 let mut channel = None;
2608 let mut device = None;
2609 let mut is_dvd = false;
2610 for opt in opts {
2611 let mut s = opt.split('=');
2612 let opt = s.next().unwrap();
2613 match opt {
2614 "ro" => read_only = true,
2615 "p" => channel = Some(0),
2616 "s" => channel = Some(1),
2617 "0" => device = Some(0),
2618 "1" => device = Some(1),
2619 "dvd" => {
2620 is_dvd = true;
2621 read_only = true;
2622 }
2623 _ => anyhow::bail!("unknown option: '{opt}'"),
2624 }
2625 }
2626
2627 Ok(IdeDiskCli {
2628 kind,
2629 read_only,
2630 channel,
2631 device,
2632 is_dvd,
2633 })
2634 }
2635}
2636
2637#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
2639pub struct FloppyDiskCli {
2640 #[kv(positional)]
2641 pub kind: DiskCliKind,
2642 #[kv(flag, key = "ro")]
2643 pub read_only: bool,
2644}
2645
2646#[derive(Clone)]
2647pub struct DebugconSerialConfigCli {
2648 pub port: u16,
2649 pub serial: SerialConfigCli,
2650}
2651
2652impl FromStr for DebugconSerialConfigCli {
2653 type Err = String;
2654
2655 fn from_str(s: &str) -> Result<Self, Self::Err> {
2656 let Some((port, serial)) = s.split_once(',') else {
2657 return Err("invalid format (missing comma between port and serial)".into());
2658 };
2659
2660 let port: u16 = parse_number(port)
2661 .map_err(|_| "could not parse port".to_owned())?
2662 .try_into()
2663 .map_err(|_| "port must be 16-bit")?;
2664 let serial: SerialConfigCli = serial.parse()?;
2665
2666 Ok(Self { port, serial })
2667 }
2668}
2669
2670#[derive(Clone, Debug, PartialEq)]
2673pub struct ComSerialConfigCli {
2674 pub debugger_mode: bool,
2676 pub backend: SerialConfigCli,
2678}
2679
2680impl FromStr for ComSerialConfigCli {
2681 type Err = String;
2682
2683 fn from_str(s: &str) -> Result<Self, Self::Err> {
2684 match s.strip_prefix("debugger-mode:") {
2685 Some(rest) => Ok(Self {
2686 debugger_mode: true,
2687 backend: rest.parse()?,
2688 }),
2689 None => Ok(Self {
2690 debugger_mode: false,
2691 backend: s.parse()?,
2692 }),
2693 }
2694 }
2695}
2696
2697#[derive(Clone, Debug, PartialEq)]
2699pub enum SerialConfigCli {
2700 None,
2701 Console,
2702 NewConsole(Option<PathBuf>, Option<String>),
2703 Stderr,
2704 Pipe(PathBuf),
2705 Tcp(SocketAddr),
2706 File(PathBuf),
2707}
2708
2709impl FromStr for SerialConfigCli {
2710 type Err = String;
2711
2712 fn from_str(s: &str) -> Result<Self, Self::Err> {
2713 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
2714
2715 let first_key = match keyvalues.first() {
2716 Some(first_pair) => first_pair.0.as_str(),
2717 None => Err("invalid serial configuration: no values supplied")?,
2718 };
2719 let first_value = keyvalues.first().unwrap().1.as_ref();
2720
2721 let ret = match first_key {
2722 "none" => SerialConfigCli::None,
2723 "console" => SerialConfigCli::Console,
2724 "stderr" => SerialConfigCli::Stderr,
2725 "file" => match first_value {
2726 Some(path) => SerialConfigCli::File(path.into()),
2727 None => Err("invalid serial configuration: file requires a value")?,
2728 },
2729 "term" => {
2730 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
2732 let window_name = match window_name {
2733 Some((_, Some(name))) => Some(name.clone()),
2734 _ => None,
2735 };
2736
2737 SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
2738 }
2739 "listen" => match first_value {
2740 Some(path) => {
2741 if let Some(tcp) = path.strip_prefix("tcp:") {
2742 let addr = tcp
2743 .parse()
2744 .map_err(|err| format!("invalid tcp address: {err}"))?;
2745 SerialConfigCli::Tcp(addr)
2746 } else {
2747 SerialConfigCli::Pipe(path.into())
2748 }
2749 }
2750 None => Err(
2751 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
2752 )?,
2753 },
2754 _ => {
2755 return Err(format!(
2756 "invalid serial configuration: '{}' is not a known option",
2757 first_key
2758 ));
2759 }
2760 };
2761
2762 Ok(ret)
2763 }
2764}
2765
2766impl SerialConfigCli {
2767 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
2770 let mut ret = Vec::new();
2771
2772 for item in s.split(',') {
2774 let mut eqsplit = item.split('=');
2777 let key = eqsplit.next();
2778 let value = eqsplit.next();
2779
2780 if let Some(key) = key {
2781 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
2782 } else {
2783 return Err("invalid key=value pair in serial config".into());
2785 }
2786 }
2787 Ok(ret)
2788 }
2789}
2790
2791#[derive(Clone, Debug, PartialEq)]
2792pub enum EndpointConfigCli {
2793 None,
2794 Consomme {
2795 cidr: Option<String>,
2796 host_fwd: Vec<HostPortConfigCli>,
2797 },
2798 Dio {
2799 id: Option<String>,
2800 },
2801 Tap {
2802 name: String,
2803 },
2804}
2805
2806#[derive(Clone, Debug, PartialEq)]
2808pub struct HostPortConfigCli {
2809 pub protocol: HostPortProtocolCli,
2810 pub host_address: Option<std::net::IpAddr>,
2811 pub host_port: u16,
2812 pub guest_port: u16,
2813}
2814
2815#[derive(Clone, Debug, PartialEq)]
2817pub enum HostPortProtocolCli {
2818 Tcp,
2819 Udp,
2820}
2821
2822fn parse_hostfwd(s: &str) -> Result<HostPortConfigCli, String> {
2823 let (host_part, guest_part) = s.split_once('-').ok_or_else(|| {
2826 format!(
2827 "invalid hostfwd format '{s}', \
2828 expected 'proto:[hostaddr]:hostport-[guestaddr]:guestport'"
2829 )
2830 })?;
2831
2832 let (proto, host_addr_port) = host_part.split_once(':').ok_or_else(|| {
2834 format!("invalid hostfwd host part '{host_part}', expected 'proto:[hostaddr]:hostport'")
2835 })?;
2836 let protocol = match proto {
2837 "tcp" => HostPortProtocolCli::Tcp,
2838 "udp" => HostPortProtocolCli::Udp,
2839 other => {
2840 return Err(format!(
2841 "unknown hostfwd protocol '{other}', expected 'tcp' or 'udp'"
2842 ));
2843 }
2844 };
2845
2846 let (host_address, host_port) = parse_addr_port(host_addr_port)
2847 .map_err(|e| format!("invalid hostfwd host address/port: {e}"))?;
2848 let (_, guest_port) = parse_addr_port(guest_part)
2849 .map_err(|e| format!("invalid hostfwd guest address/port: {e}"))?;
2850
2851 Ok(HostPortConfigCli {
2852 protocol,
2853 host_address,
2854 host_port,
2855 guest_port,
2856 })
2857}
2858
2859fn parse_addr_port(s: &str) -> Result<(Option<std::net::IpAddr>, u16), String> {
2865 if let Some(rest) = s.strip_prefix('[') {
2866 let (addr, port) = rest
2868 .split_once("]:")
2869 .ok_or_else(|| format!("expected '[addr]:port', got '[{rest}'"))?;
2870 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
2871 let addr: std::net::IpAddr = addr
2872 .parse()
2873 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
2874 Ok((Some(addr), port))
2875 } else {
2876 match s.rsplit_once(':') {
2877 Some((addr, port)) => {
2878 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
2879 let addr = if addr.is_empty() {
2880 None
2881 } else {
2882 let parsed: std::net::IpAddr = addr
2883 .parse()
2884 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
2885 Some(parsed)
2886 };
2887 Ok((addr, port))
2888 }
2889 None => {
2890 let port: u16 = s.parse().map_err(|_| format!("invalid port '{s}'"))?;
2891 Ok((None, port))
2892 }
2893 }
2894 }
2895}
2896
2897impl FromStr for EndpointConfigCli {
2898 type Err = String;
2899
2900 fn from_str(s: &str) -> Result<Self, Self::Err> {
2901 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
2902 ["none"] => EndpointConfigCli::None,
2903 ["consomme", rest @ ..] => {
2904 let remaining = rest.join(":");
2905 let mut cidr = None;
2906 let mut host_fwd = Vec::new();
2907 for opt in remaining.split(',').filter(|s| !s.is_empty()) {
2908 if let Some(fwd) = opt.strip_prefix("hostfwd=") {
2909 host_fwd.push(parse_hostfwd(fwd)?);
2910 } else if cidr.is_none() {
2911 cidr = Some(opt.to_owned());
2912 } else {
2913 return Err(format!("unexpected consomme option '{opt}'"));
2914 }
2915 }
2916 EndpointConfigCli::Consomme { cidr, host_fwd }
2917 }
2918 ["dio", s @ ..] => EndpointConfigCli::Dio {
2919 id: s.first().map(|s| (*s).to_owned()),
2920 },
2921 ["tap", name] => EndpointConfigCli::Tap {
2922 name: (*name).to_owned(),
2923 },
2924 _ => return Err("invalid network backend".into()),
2925 };
2926
2927 Ok(ret)
2928 }
2929}
2930
2931#[derive(Clone, Debug, PartialEq)]
2932pub struct NicConfigCli {
2933 pub vtl: DeviceVtl,
2934 pub endpoint: EndpointConfigCli,
2935 pub max_queues: Option<u16>,
2936 pub underhill: bool,
2937 pub pcie_port: Option<String>,
2938}
2939
2940impl FromStr for NicConfigCli {
2941 type Err = String;
2942
2943 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
2944 let mut vtl = DeviceVtl::Vtl0;
2945 let mut max_queues = None;
2946 let mut underhill = false;
2947 let mut pcie_port = None;
2948 while let Some((opt, rest)) = s.split_once(':') {
2949 if let Some((opt, val)) = opt.split_once('=') {
2950 match opt {
2951 "queues" => {
2952 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
2953 }
2954 "pcie_port" => {
2955 if val.is_empty() {
2956 return Err("`pcie_port=` requires port name argument".into());
2957 }
2958 pcie_port = Some(val.to_string());
2959 }
2960 _ => break,
2961 }
2962 } else {
2963 match opt {
2964 "vtl2" => {
2965 vtl = DeviceVtl::Vtl2;
2966 }
2967 "uh" => underhill = true,
2968 _ => break,
2969 }
2970 }
2971 s = rest;
2972 }
2973
2974 if underhill && vtl != DeviceVtl::Vtl0 {
2975 return Err("`uh` is incompatible with `vtl2`".into());
2976 }
2977
2978 if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
2979 return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
2980 }
2981
2982 let endpoint = s.parse()?;
2983 Ok(NicConfigCli {
2984 vtl,
2985 endpoint,
2986 max_queues,
2987 underhill,
2988 pcie_port,
2989 })
2990 }
2991}
2992
2993#[derive(Debug, Error)]
2994#[error("unknown VTL2 relocation type: {0}")]
2995pub struct UnknownVtl2RelocationType(String);
2996
2997fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
2998 match s {
2999 "disable" => Ok(Vtl2BaseAddressType::File),
3000 s if s.starts_with("auto=") => {
3001 let s = s.strip_prefix("auto=").unwrap_or_default();
3002 let size = if s == "filesize" {
3003 None
3004 } else {
3005 let size = parse_memory(s).map_err(|e| {
3006 UnknownVtl2RelocationType(format!(
3007 "unable to parse memory size from {} for 'auto=' type, {e}",
3008 e
3009 ))
3010 })?;
3011 Some(size)
3012 };
3013 Ok(Vtl2BaseAddressType::MemoryLayout { size })
3014 }
3015 s if s.starts_with("absolute=") => {
3016 let s = s.strip_prefix("absolute=");
3017 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
3018 UnknownVtl2RelocationType(format!(
3019 "unable to parse number from {} for 'absolute=' type",
3020 e
3021 ))
3022 })?;
3023 Ok(Vtl2BaseAddressType::Absolute(addr))
3024 }
3025 s if s.starts_with("vtl2=") => {
3026 let s = s.strip_prefix("vtl2=").unwrap_or_default();
3027 let size = if s == "filesize" {
3028 None
3029 } else {
3030 let size = parse_memory(s).map_err(|e| {
3031 UnknownVtl2RelocationType(format!(
3032 "unable to parse memory size from {} for 'vtl2=' type, {e}",
3033 e
3034 ))
3035 })?;
3036 Some(size)
3037 };
3038 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
3039 }
3040 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
3041 }
3042}
3043
3044#[derive(Debug, Copy, Clone, PartialEq)]
3045pub enum SmtConfigCli {
3046 Auto,
3047 Force,
3048 Off,
3049}
3050
3051#[derive(Debug, Error)]
3052#[error("expected auto, force, or off")]
3053pub struct BadSmtConfig;
3054
3055impl FromStr for SmtConfigCli {
3056 type Err = BadSmtConfig;
3057
3058 fn from_str(s: &str) -> Result<Self, Self::Err> {
3059 let r = match s {
3060 "auto" => Self::Auto,
3061 "force" => Self::Force,
3062 "off" => Self::Off,
3063 _ => return Err(BadSmtConfig),
3064 };
3065 Ok(r)
3066 }
3067}
3068
3069#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
3070fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
3071 let r = match s {
3072 "auto" => X2ApicConfig::Auto,
3073 "supported" => X2ApicConfig::Supported,
3074 "off" => X2ApicConfig::Unsupported,
3075 "on" => X2ApicConfig::Enabled,
3076 _ => return Err("expected auto, supported, off, or on"),
3077 };
3078 Ok(r)
3079}
3080
3081#[derive(Debug, Copy, Clone, ValueEnum)]
3082pub enum Vtl0LateMapPolicyCli {
3083 Off,
3084 Log,
3085 Halt,
3086 Exception,
3087}
3088
3089#[derive(Debug, Copy, Clone, Default, ValueEnum)]
3091pub enum GicMsiCli {
3092 #[default]
3094 Auto,
3095 Its,
3097 V2m,
3099}
3100
3101#[derive(Debug, Copy, Clone, ValueEnum)]
3102pub enum IsolationCli {
3103 Vbs,
3104 Snp,
3105}
3106
3107#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
3108pub enum IgvmPersonalityCli {
3109 Uefi,
3110 LinuxDirect,
3111}
3112
3113#[derive(Debug, Copy, Clone, PartialEq)]
3114pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
3115
3116impl FromStr for PcatBootOrderCli {
3117 type Err = &'static str;
3118
3119 fn from_str(s: &str) -> Result<Self, Self::Err> {
3120 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
3121 let mut order = Vec::new();
3122
3123 for item in s.split(',') {
3124 let device = match item {
3125 "optical" => PcatBootDevice::Optical,
3126 "hdd" => PcatBootDevice::HardDrive,
3127 "net" => PcatBootDevice::Network,
3128 "floppy" => PcatBootDevice::Floppy,
3129 _ => return Err("unknown boot device type"),
3130 };
3131
3132 let default_pos = default_order
3133 .iter()
3134 .position(|x| x == &Some(device))
3135 .ok_or("cannot pass duplicate boot devices")?;
3136
3137 order.push(default_order[default_pos].take().unwrap());
3138 }
3139
3140 order.extend(default_order.into_iter().flatten());
3141 assert_eq!(order.len(), 4);
3142
3143 Ok(Self(order.try_into().unwrap()))
3144 }
3145}
3146
3147#[derive(Copy, Clone, Debug, ValueEnum)]
3148pub enum UefiConsoleModeCli {
3149 Default,
3150 Com1,
3151 Com2,
3152 None,
3153}
3154
3155#[derive(Copy, Clone, Debug, Default, ValueEnum)]
3156pub enum EfiDiagnosticsLogLevelCli {
3157 #[default]
3158 Default,
3159 Info,
3160 Full,
3161}
3162
3163#[derive(Clone, Debug, PartialEq)]
3164pub struct PcieRootComplexCli {
3165 pub name: String,
3166 pub segment: u16,
3167 pub start_bus: u8,
3168 pub end_bus: u8,
3169 pub low_mmio: u32,
3170 pub high_mmio: u64,
3171 pub low_mmio_base: Option<u64>,
3172 pub high_mmio_base: Option<u64>,
3173 pub preserve_bars: bool,
3174 pub hdm: u64,
3175 pub hdm_window_restrictions: CfmwsWindowRestrictions,
3176 pub vnode: Option<u32>,
3177}
3178
3179struct HexAddress(u64);
3181
3182impl FromStr for HexAddress {
3183 type Err = anyhow::Error;
3184
3185 fn from_str(s: &str) -> anyhow::Result<Self> {
3186 Ok(HexAddress(parse_address(s)?))
3187 }
3188}
3189
3190struct CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions);
3192
3193impl FromStr for CfmwsWindowRestrictionsCli {
3194 type Err = anyhow::Error;
3195
3196 fn from_str(s: &str) -> anyhow::Result<Self> {
3197 Ok(CfmwsWindowRestrictionsCli(
3198 parse_cxl_cfmws_window_restriction_u16_bitmask(s)?,
3199 ))
3200 }
3201}
3202
3203#[derive(vmm_cli::KeyValueArgs)]
3206struct PcieRootComplexArgs {
3207 #[kv(positional)]
3208 name: String,
3209 #[kv(default)]
3210 segment: u16,
3211 #[kv(default)]
3212 start_bus: u8,
3213 #[kv(default = 255)]
3214 end_bus: u8,
3215 #[kv(default = vmm_cli::MemorySize(64 * 1024 * 1024))]
3216 low_mmio: vmm_cli::MemorySize,
3217 #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
3218 high_mmio: vmm_cli::MemorySize,
3219 low_mmio_base: Option<HexAddress>,
3220 high_mmio_base: Option<HexAddress>,
3221 #[kv(flag)]
3222 preserve_bars: bool,
3223 #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
3224 hdm: vmm_cli::MemorySize,
3225 #[kv(default = CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions::DEVICE_COHERENT))]
3226 hdm_window_restrictions: CfmwsWindowRestrictionsCli,
3227 #[kv(key = "node")]
3228 vnode: Option<u32>,
3229}
3230
3231impl FromStr for PcieRootComplexCli {
3232 type Err = anyhow::Error;
3233
3234 fn from_str(s: &str) -> Result<Self, Self::Err> {
3235 let args: PcieRootComplexArgs = s.parse()?;
3236
3237 if args.start_bus > args.end_bus {
3238 anyhow::bail!("start_bus must be <= end_bus");
3239 }
3240
3241 let low_mmio = u32::try_from(args.low_mmio.0).context("low MMIO size exceeds 32 bits")?;
3242
3243 Ok(PcieRootComplexCli {
3244 name: args.name,
3245 segment: args.segment,
3246 start_bus: args.start_bus,
3247 end_bus: args.end_bus,
3248 low_mmio,
3249 high_mmio: args.high_mmio.0,
3250 low_mmio_base: args.low_mmio_base.map(|a| a.0),
3251 high_mmio_base: args.high_mmio_base.map(|a| a.0),
3252 preserve_bars: args.preserve_bars,
3253 hdm: args.hdm.0,
3254 hdm_window_restrictions: args.hdm_window_restrictions.0,
3255 vnode: args.vnode,
3256 })
3257 }
3258}
3259
3260fn parse_cxl_cfmws_window_restriction_u16_bitmask(
3261 s: &str,
3262) -> anyhow::Result<CfmwsWindowRestrictions> {
3263 let bits = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
3264 u16::from_str_radix(hex, 16).context("invalid hex bitmask")?
3265 } else {
3266 u16::from_str(s).context("invalid decimal bitmask")?
3267 };
3268
3269 CfmwsWindowRestrictions::try_from_bits(bits)
3270 .context("bitmask includes reserved CFMWS window restriction bits")
3271}
3272
3273#[derive(Clone, Debug, PartialEq)]
3274pub struct PcieRootPortCli {
3275 pub root_complex_name: String,
3276 pub name: String,
3277 pub devfn: Option<u8>,
3278 pub hotplug: bool,
3279 pub acs_capabilities_supported: Option<u16>,
3280 pub cxl: bool,
3281 pub pasid: bool,
3282}
3283
3284struct PortNamePair {
3287 parent: String,
3288 child: String,
3289}
3290
3291impl FromStr for PortNamePair {
3292 type Err = anyhow::Error;
3293
3294 fn from_str(s: &str) -> anyhow::Result<Self> {
3295 let mut it = s.split(':');
3296 let parent = it
3297 .next()
3298 .filter(|x| !x.is_empty())
3299 .context("expected parent name")?;
3300 let child = it
3301 .next()
3302 .filter(|x| !x.is_empty())
3303 .context("expected child name")?;
3304 anyhow::ensure!(it.next().is_none(), "unexpected token in '{s}'");
3305 Ok(PortNamePair {
3306 parent: parent.to_string(),
3307 child: child.to_string(),
3308 })
3309 }
3310}
3311
3312struct PcieAddr(u8);
3314
3315impl FromStr for PcieAddr {
3316 type Err = anyhow::Error;
3317
3318 fn from_str(s: &str) -> anyhow::Result<Self> {
3319 Ok(PcieAddr(parse_pcie_addr(s)?))
3320 }
3321}
3322
3323struct AcsMask(u16);
3325
3326impl FromStr for AcsMask {
3327 type Err = anyhow::Error;
3328
3329 fn from_str(s: &str) -> anyhow::Result<Self> {
3330 Ok(AcsMask(parse_acs_capability_mask(s)?))
3331 }
3332}
3333
3334#[derive(vmm_cli::KeyValueArgs)]
3336struct RootPortArgs {
3337 #[kv(positional)]
3338 names: PortNamePair,
3339 addr: Option<PcieAddr>,
3340 #[kv(flag)]
3341 hotplug: bool,
3342 acs: Option<AcsMask>,
3343 #[kv(flag)]
3344 cxl: bool,
3345 #[kv(flag)]
3346 pasid: bool,
3347}
3348
3349impl FromStr for PcieRootPortCli {
3350 type Err = anyhow::Error;
3351
3352 fn from_str(s: &str) -> Result<Self, Self::Err> {
3353 let args: RootPortArgs = s.parse()?;
3354 Ok(PcieRootPortCli {
3355 root_complex_name: args.names.parent,
3356 name: args.names.child,
3357 devfn: args.addr.map(|a| a.0),
3358 hotplug: args.hotplug,
3359 acs_capabilities_supported: args.acs.map(|a| a.0),
3360 cxl: args.cxl,
3361 pasid: args.pasid,
3362 })
3363 }
3364}
3365
3366fn parse_pcie_addr(s: &str) -> anyhow::Result<u8> {
3370 let parse_int = |v: &str| -> anyhow::Result<u8> {
3371 if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) {
3372 u8::from_str_radix(hex, 16).context("invalid hex number")
3373 } else {
3374 v.parse().context("invalid number")
3375 }
3376 };
3377
3378 let mut parts = s.split('.');
3379 let device = parse_int(parts.next().context("expected device number")?)?;
3380 let function = match parts.next() {
3381 Some(f) => parse_int(f)?,
3382 None => 0,
3383 };
3384 if parts.next().is_some() {
3385 anyhow::bail!("unexpected token in addr '{s}'");
3386 }
3387 if device > 31 {
3388 anyhow::bail!("device number {device} out of range (0-31)");
3389 }
3390 if function > 7 {
3391 anyhow::bail!("function number {function} out of range (0-7)");
3392 }
3393 Ok((device << 3) | function)
3394}
3395
3396#[derive(Clone, Debug, PartialEq)]
3397pub struct GenericPcieSwitchCli {
3398 pub port_name: String,
3399 pub name: String,
3400 pub num_downstream_ports: u8,
3401 pub hotplug: bool,
3402 pub acs_capabilities_supported: Option<u16>,
3403 pub pasid: bool,
3404}
3405
3406#[derive(vmm_cli::KeyValueArgs)]
3408struct SwitchArgs {
3409 #[kv(positional)]
3410 names: PortNamePair,
3411 #[kv(default = 4)]
3412 num_downstream_ports: u8,
3413 #[kv(flag)]
3414 hotplug: bool,
3415 acs: Option<AcsMask>,
3416 #[kv(flag)]
3417 pasid: bool,
3418}
3419
3420impl FromStr for GenericPcieSwitchCli {
3421 type Err = anyhow::Error;
3422
3423 fn from_str(s: &str) -> Result<Self, Self::Err> {
3424 let args: SwitchArgs = s.parse()?;
3425 Ok(GenericPcieSwitchCli {
3426 port_name: args.names.parent,
3427 name: args.names.child,
3428 num_downstream_ports: args.num_downstream_ports,
3429 hotplug: args.hotplug,
3430 acs_capabilities_supported: args.acs.map(|a| a.0),
3431 pasid: args.pasid,
3432 })
3433 }
3434}
3435
3436#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3438pub struct PcieGenericInitiatorCli {
3439 #[kv(key = "port")]
3442 pub port_name: String,
3443 pub node: u32,
3445}
3446
3447#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3449pub struct PcieRemoteCli {
3450 #[kv(positional)]
3452 pub port_name: String,
3453 #[kv(key = "socket")]
3455 pub socket_addr: Option<String>,
3456 #[kv(default)]
3458 pub hu: u16,
3459 #[kv(default)]
3461 pub controller: u16,
3462}
3463
3464#[cfg(target_os = "linux")]
3468#[derive(Clone, Debug)]
3469pub struct VfioDeviceCli {
3470 pub port_name: String,
3472 pub pci_id: String,
3474 pub iommu: Option<String>,
3477 pub bar_addresses: [vfio_assigned_device_resources::BarAddressConfig; 6],
3479}
3480
3481#[cfg(target_os = "linux")]
3483struct BarAddressCli(vfio_assigned_device_resources::BarAddressConfig);
3484
3485#[cfg(target_os = "linux")]
3486impl FromStr for BarAddressCli {
3487 type Err = anyhow::Error;
3488
3489 fn from_str(s: &str) -> anyhow::Result<Self> {
3490 let config = if s == "host" {
3491 vfio_assigned_device_resources::BarAddressConfig::HostAssigned
3492 } else if let Some(value) = s.strip_prefix("0x") {
3493 let address = u64::from_str_radix(value, 16).context("invalid BAR address")?;
3494 anyhow::ensure!(address != 0, "BAR address must be nonzero");
3495 vfio_assigned_device_resources::BarAddressConfig::Fixed(address)
3496 } else {
3497 anyhow::bail!("expected 'host' or a hexadecimal address starting with '0x'");
3498 };
3499 Ok(Self(config))
3500 }
3501}
3502
3503#[cfg(target_os = "linux")]
3506#[derive(vmm_cli::KeyValueArgs)]
3507struct BarFlags {
3508 bar0: Option<BarAddressCli>,
3509 bar1: Option<BarAddressCli>,
3510 bar2: Option<BarAddressCli>,
3511 bar3: Option<BarAddressCli>,
3512 bar4: Option<BarAddressCli>,
3513 bar5: Option<BarAddressCli>,
3514}
3515
3516#[cfg(target_os = "linux")]
3518#[derive(vmm_cli::KeyValueArgs)]
3519struct VfioArgs {
3520 host: String,
3521 port: String,
3522 iommu: Option<String>,
3523 #[kv(flatten)]
3524 bars: BarFlags,
3525}
3526
3527#[cfg(target_os = "linux")]
3528impl FromStr for VfioDeviceCli {
3529 type Err = anyhow::Error;
3530
3531 fn from_str(s: &str) -> Result<Self, Self::Err> {
3532 let args: VfioArgs = s.parse()?;
3533
3534 if args.host.contains('/') || args.host.contains("..") {
3536 anyhow::bail!("PCI address must not contain path separators");
3537 }
3538
3539 let bars = args.bars;
3540 let bar_addresses = [
3541 bars.bar0.map(|bar| bar.0).unwrap_or_default(),
3542 bars.bar1.map(|bar| bar.0).unwrap_or_default(),
3543 bars.bar2.map(|bar| bar.0).unwrap_or_default(),
3544 bars.bar3.map(|bar| bar.0).unwrap_or_default(),
3545 bars.bar4.map(|bar| bar.0).unwrap_or_default(),
3546 bars.bar5.map(|bar| bar.0).unwrap_or_default(),
3547 ];
3548
3549 Ok(VfioDeviceCli {
3550 port_name: args.port,
3551 pci_id: args.host,
3552 iommu: args.iommu,
3553 bar_addresses,
3554 })
3555 }
3556}
3557
3558#[cfg(guest_arch = "aarch64")]
3562#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
3563pub struct SmmuCli {
3564 #[kv(key = "rc")]
3566 pub rc_name: String,
3567 #[kv(flag)]
3569 pub accel: bool,
3570 #[kv(default)]
3572 pub oas: SmmuOasCli,
3573}
3574
3575#[cfg(guest_arch = "aarch64")]
3577#[derive(Clone, Copy, Debug, Default)]
3578pub enum SmmuOasCli {
3579 #[default]
3582 Auto,
3583 Fixed(u8),
3585}
3586
3587#[cfg(guest_arch = "aarch64")]
3588impl FromStr for SmmuOasCli {
3589 type Err = anyhow::Error;
3590
3591 fn from_str(s: &str) -> Result<Self, Self::Err> {
3592 Ok(if s == "auto" {
3593 SmmuOasCli::Auto
3594 } else {
3595 SmmuOasCli::Fixed(s.parse().context("oas must be 'auto' or a number")?)
3596 })
3597 }
3598}
3599
3600#[cfg(target_os = "linux")]
3604#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
3605pub struct IommuCli {
3606 pub id: String,
3608}
3609
3610fn default_value_from_arch_env(name: &str) -> OsString {
3618 let prefix = if cfg!(guest_arch = "x86_64") {
3619 "X86_64"
3620 } else if cfg!(guest_arch = "aarch64") {
3621 "AARCH64"
3622 } else {
3623 return Default::default();
3624 };
3625 let prefixed = format!("{}_{}", prefix, name);
3626 std::env::var_os(name)
3627 .or_else(|| std::env::var_os(prefixed))
3628 .unwrap_or_default()
3629}
3630
3631#[derive(Clone)]
3633pub struct OptionalPathBuf(pub Option<PathBuf>);
3634
3635impl From<&std::ffi::OsStr> for OptionalPathBuf {
3636 fn from(s: &std::ffi::OsStr) -> Self {
3637 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
3638 }
3639}
3640
3641#[cfg(target_os = "linux")]
3642#[derive(Clone)]
3643pub enum VhostUserDeviceTypeCli {
3644 Blk {
3647 num_queues: Option<u16>,
3648 queue_size: Option<u16>,
3649 },
3650 Fs {
3652 tag: String,
3653 num_queues: Option<u16>,
3654 queue_size: Option<u16>,
3655 },
3656 Other {
3658 device_id: u16,
3659 queue_sizes: Vec<u16>,
3660 },
3661}
3662
3663#[cfg(target_os = "linux")]
3664#[derive(Clone)]
3665pub struct VhostUserCli {
3666 pub socket_path: String,
3667 pub device_type: VhostUserDeviceTypeCli,
3668 pub pcie_port: Option<String>,
3669}
3670
3671#[cfg(target_os = "linux")]
3674#[derive(vmm_cli::KeyValueArgs)]
3675struct VhostUserArgs {
3676 #[kv(positional)]
3677 socket_path: String,
3678 #[kv(key = "type")]
3679 type_name: Option<String>,
3680 device_id: Option<u16>,
3681 tag: Option<String>,
3682 pcie_port: Option<String>,
3683 num_queues: Option<u16>,
3684 queue_size: Option<u16>,
3685 queue_sizes: Option<vmm_cli::BracketList<u16>>,
3686}
3687
3688#[cfg(target_os = "linux")]
3689impl FromStr for VhostUserCli {
3690 type Err = anyhow::Error;
3691
3692 fn from_str(s: &str) -> anyhow::Result<Self> {
3693 let mut args: VhostUserArgs = s.parse()?;
3694 let type_name = args.type_name.take();
3695
3696 if type_name.is_some() == args.device_id.is_some() {
3697 anyhow::bail!("must specify type=<name> or device_id=<N>");
3698 }
3699
3700 let device_type = match type_name.as_deref() {
3703 Some("fs") => VhostUserDeviceTypeCli::Fs {
3704 tag: args.tag.take().context("type=fs requires tag=<name>")?,
3705 num_queues: args.num_queues.take(),
3706 queue_size: args.queue_size.take(),
3707 },
3708 Some("blk") => VhostUserDeviceTypeCli::Blk {
3709 num_queues: args.num_queues.take(),
3710 queue_size: args.queue_size.take(),
3711 },
3712 Some(ty) => anyhow::bail!("unknown vhost-user device type: '{ty}'"),
3713 None => {
3714 let queue_sizes = args
3715 .queue_sizes
3716 .take()
3717 .context("device_id= requires queue_sizes=[N,N,...]")?
3718 .0;
3719 anyhow::ensure!(!queue_sizes.is_empty(), "queue_sizes must be non-empty");
3720 VhostUserDeviceTypeCli::Other {
3721 device_id: args.device_id.unwrap(),
3722 queue_sizes,
3723 }
3724 }
3725 };
3726
3727 if args.tag.is_some() {
3728 anyhow::bail!("tag= is only valid for type=fs");
3729 }
3730 if args.queue_sizes.is_some() {
3731 anyhow::bail!("queue_sizes= is only valid for device_id=");
3732 }
3733 if args.num_queues.is_some() || args.queue_size.is_some() {
3734 anyhow::bail!(
3735 "num_queues= and queue_size= are not valid for device_id=; use queue_sizes="
3736 );
3737 }
3738
3739 Ok(VhostUserCli {
3740 socket_path: args.socket_path,
3741 device_type,
3742 pcie_port: args.pcie_port,
3743 })
3744 }
3745}
3746
3747#[cfg(test)]
3748mod tests {
3749 use super::*;
3750
3751 use std::path::Path;
3752 use test_with_tracing::test;
3753
3754 #[test]
3757 fn version_reports_build_info() {
3758 let short = version_output(["openvmm", "-V"]);
3759 assert_eq!(
3760 short,
3761 format!("openvmm {}\n", openvmm_build_info::get().version())
3762 );
3763
3764 let long = version_output(["openvmm", "--version"]);
3765 assert_eq!(
3766 long,
3767 format!("openvmm {}\n", openvmm_build_info::get().long_version())
3768 );
3769 }
3770
3771 fn version_output(args: [&str; 2]) -> String {
3772 let Err(error) = Options::try_parse_from(args) else {
3773 panic!("{args:?} unexpectedly parsed as runtime options");
3774 };
3775 assert_eq!(error.kind(), clap::error::ErrorKind::DisplayVersion);
3776 error.to_string()
3777 }
3778
3779 #[test]
3780 fn test_parse_rpc() {
3781 let rpc = RpcCli::from_str("path=/tmp/openvmm.sock").unwrap();
3783 assert_eq!(rpc.path, Path::new("/tmp/openvmm.sock"));
3784 assert_eq!(rpc.transport, RpcTransportCli::Auto);
3785
3786 for (s, transport) in [
3788 ("auto", RpcTransportCli::Auto),
3789 ("ttrpc", RpcTransportCli::Ttrpc),
3790 ("grpc", RpcTransportCli::Grpc),
3791 ] {
3792 let rpc = RpcCli::from_str(&format!("path=/tmp/s.sock,transport={s}")).unwrap();
3793 assert_eq!(rpc.path, Path::new("/tmp/s.sock"));
3794 assert_eq!(rpc.transport, transport);
3795 }
3796
3797 assert!(RpcCli::from_str("").is_err());
3799 assert!(RpcCli::from_str("transport=ttrpc").is_err());
3800 assert!(RpcCli::from_str("path=").is_err());
3801 assert!(RpcCli::from_str("path=/tmp/s.sock,transport=bogus").is_err());
3802 assert!(RpcCli::from_str("path=/tmp/s.sock,bogus=1").is_err());
3803 assert!(RpcCli::from_str("path=/a,path=/b").is_err());
3804 }
3805
3806 #[test]
3807 fn test_parse_file_opts() {
3808 let disk = DiskCliKind::from_str("file:test.vhd;create=1G").unwrap();
3810 assert!(matches!(
3811 &disk,
3812 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
3813 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
3814 ));
3815
3816 let disk = DiskCliKind::from_str("test.vhd;create=1G").unwrap();
3818 assert!(matches!(
3819 &disk,
3820 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
3821 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
3822 ));
3823
3824 let disk = DiskCliKind::from_str("file:/dev/sdb;direct").unwrap();
3826 assert!(matches!(
3827 &disk,
3828 DiskCliKind::File { path, create_with_len: None, direct: true }
3829 if path == Path::new("/dev/sdb")
3830 ));
3831
3832 let disk = DiskCliKind::from_str("file:disk.img;direct;create=1G").unwrap();
3834 assert!(matches!(
3835 &disk,
3836 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
3837 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
3838 ));
3839
3840 let disk = DiskCliKind::from_str("file:disk.img;create=1G;direct").unwrap();
3841 assert!(matches!(
3842 &disk,
3843 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
3844 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
3845 ));
3846
3847 let disk = DiskCliKind::from_str("file:disk.img").unwrap();
3849 assert!(matches!(
3850 &disk,
3851 DiskCliKind::File { path, create_with_len: None, direct: false }
3852 if path == Path::new("disk.img")
3853 ));
3854
3855 assert!(DiskCliKind::from_str("file:disk.img;bogus").is_err());
3857
3858 assert!(DiskCliKind::from_str("sql:db.sqlite;direct").is_err());
3860 }
3861
3862 #[test]
3863 fn test_parse_memory_disk() {
3864 let s = "mem:1G";
3865 let disk = DiskCliKind::from_str(s).unwrap();
3866 match disk {
3867 DiskCliKind::Memory(size) => {
3868 assert_eq!(size, 1024 * 1024 * 1024); }
3870 _ => panic!("Expected Memory variant"),
3871 }
3872 }
3873
3874 #[test]
3875 fn test_parse_pcie_disk() {
3876 assert_eq!(
3877 DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
3878 Some("p0".to_string())
3879 );
3880 assert_eq!(
3881 DiskCli::from_str("file:path.vhdx,pcie_port=p0")
3882 .unwrap()
3883 .pcie_port,
3884 Some("p0".to_string())
3885 );
3886 assert_eq!(
3887 DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
3888 .unwrap()
3889 .pcie_port,
3890 Some("p0".to_string())
3891 );
3892
3893 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
3895
3896 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
3898 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
3899 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
3900 }
3901
3902 #[test]
3903 fn test_parse_memory_diff_disk() {
3904 let s = "memdiff:file:base.img";
3905 let disk = DiskCliKind::from_str(s).unwrap();
3906 match disk {
3907 DiskCliKind::MemoryDiff(inner) => match *inner {
3908 DiskCliKind::File {
3909 path,
3910 create_with_len,
3911 ..
3912 } => {
3913 assert_eq!(path, PathBuf::from("base.img"));
3914 assert_eq!(create_with_len, None);
3915 }
3916 _ => panic!("Expected File variant inside MemoryDiff"),
3917 },
3918 _ => panic!("Expected MemoryDiff variant"),
3919 }
3920 }
3921
3922 #[test]
3923 fn test_parse_sqlite_disk() {
3924 let s = "sql:db.sqlite;create=2G";
3925 let disk = DiskCliKind::from_str(s).unwrap();
3926 match disk {
3927 DiskCliKind::Sqlite {
3928 path,
3929 create_with_len,
3930 } => {
3931 assert_eq!(path, PathBuf::from("db.sqlite"));
3932 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
3933 }
3934 _ => panic!("Expected Sqlite variant"),
3935 }
3936
3937 let s = "sql:db.sqlite";
3939 let disk = DiskCliKind::from_str(s).unwrap();
3940 match disk {
3941 DiskCliKind::Sqlite {
3942 path,
3943 create_with_len,
3944 } => {
3945 assert_eq!(path, PathBuf::from("db.sqlite"));
3946 assert_eq!(create_with_len, None);
3947 }
3948 _ => panic!("Expected Sqlite variant"),
3949 }
3950 }
3951
3952 #[test]
3953 fn test_parse_sqlite_diff_disk() {
3954 let s = "sqldiff:diff.sqlite;create:file:base.img";
3956 let disk = DiskCliKind::from_str(s).unwrap();
3957 match disk {
3958 DiskCliKind::SqliteDiff { path, create, disk } => {
3959 assert_eq!(path, PathBuf::from("diff.sqlite"));
3960 assert!(create);
3961 match *disk {
3962 DiskCliKind::File {
3963 path,
3964 create_with_len,
3965 ..
3966 } => {
3967 assert_eq!(path, PathBuf::from("base.img"));
3968 assert_eq!(create_with_len, None);
3969 }
3970 _ => panic!("Expected File variant inside SqliteDiff"),
3971 }
3972 }
3973 _ => panic!("Expected SqliteDiff variant"),
3974 }
3975
3976 let s = "sqldiff:diff.sqlite:file:base.img";
3978 let disk = DiskCliKind::from_str(s).unwrap();
3979 match disk {
3980 DiskCliKind::SqliteDiff { path, create, disk } => {
3981 assert_eq!(path, PathBuf::from("diff.sqlite"));
3982 assert!(!create);
3983 match *disk {
3984 DiskCliKind::File {
3985 path,
3986 create_with_len,
3987 ..
3988 } => {
3989 assert_eq!(path, PathBuf::from("base.img"));
3990 assert_eq!(create_with_len, None);
3991 }
3992 _ => panic!("Expected File variant inside SqliteDiff"),
3993 }
3994 }
3995 _ => panic!("Expected SqliteDiff variant"),
3996 }
3997 }
3998
3999 #[test]
4000 fn test_parse_autocache_sqlite_disk() {
4001 let disk =
4003 DiskCliKind::parse_autocache(":file:disk.vhd", Ok("/tmp/cache".to_string())).unwrap();
4004 assert!(matches!(
4005 disk,
4006 DiskCliKind::AutoCacheSqlite {
4007 cache_path,
4008 key,
4009 disk: _disk,
4010 } if cache_path == "/tmp/cache" && key.is_none()
4011 ));
4012
4013 let disk =
4015 DiskCliKind::parse_autocache("mykey:file:disk.vhd", Ok("/tmp/cache".to_string()))
4016 .unwrap();
4017 assert!(matches!(
4018 disk,
4019 DiskCliKind::AutoCacheSqlite {
4020 cache_path,
4021 key: Some(key),
4022 disk: _disk,
4023 } if cache_path == "/tmp/cache" && key == "mykey"
4024 ));
4025
4026 assert!(
4028 DiskCliKind::parse_autocache(":file:disk.vhd", Err(std::env::VarError::NotPresent),)
4029 .is_err()
4030 );
4031 }
4032
4033 #[test]
4034 fn test_parse_disk_errors() {
4035 assert!(DiskCliKind::from_str("invalid:").is_err());
4036 assert!(DiskCliKind::from_str("memory:extra").is_err());
4037
4038 assert!(DiskCliKind::from_str("sqlite:").is_err());
4040 }
4041
4042 #[test]
4043 fn test_parse_errors() {
4044 assert!(DiskCliKind::from_str("mem:invalid").is_err());
4046
4047 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
4049
4050 assert!(
4052 DiskCliKind::parse_autocache("key:file:disk.vhd", Err(std::env::VarError::NotPresent),)
4053 .is_err()
4054 );
4055
4056 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
4058
4059 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
4061
4062 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
4064
4065 assert!(DiskCliKind::from_str("invalid:path").is_err());
4067
4068 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
4070 }
4071
4072 #[test]
4073 fn test_fs_args_from_str() {
4074 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
4075 assert_eq!(args.tag, "tag1");
4076 assert_eq!(args.path, "/path/to/fs");
4077
4078 assert!(FsArgs::from_str("tag1").is_err());
4080 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
4081 }
4082
4083 #[test]
4084 fn test_fs_args_with_options_from_str() {
4085 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
4086 assert_eq!(args.tag, "tag1");
4087 assert_eq!(args.path, "/path/to/fs");
4088 assert_eq!(args.options, "opt1;opt2");
4089
4090 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
4092 assert_eq!(args.tag, "tag1");
4093 assert_eq!(args.path, "/path/to/fs");
4094 assert_eq!(args.options, "");
4095
4096 assert!(FsArgsWithOptions::from_str("tag1").is_err());
4098 }
4099
4100 #[test]
4101 fn test_serial_config_from_str() {
4102 assert_eq!(
4103 SerialConfigCli::from_str("none").unwrap(),
4104 SerialConfigCli::None
4105 );
4106 assert_eq!(
4107 SerialConfigCli::from_str("console").unwrap(),
4108 SerialConfigCli::Console
4109 );
4110 assert_eq!(
4111 SerialConfigCli::from_str("stderr").unwrap(),
4112 SerialConfigCli::Stderr
4113 );
4114
4115 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
4117 if let SerialConfigCli::File(path) = file_config {
4118 assert_eq!(path.to_str().unwrap(), "/path/to/file");
4119 } else {
4120 panic!("Expected File variant");
4121 }
4122
4123 match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
4125 SerialConfigCli::NewConsole(None, Some(name)) => {
4126 assert_eq!(name, "MyTerm");
4127 }
4128 _ => panic!("Expected NewConsole variant with name"),
4129 }
4130
4131 match SerialConfigCli::from_str("term").unwrap() {
4133 SerialConfigCli::NewConsole(None, None) => (),
4134 _ => panic!("Expected NewConsole variant without name"),
4135 }
4136
4137 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
4139 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
4140 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
4141 assert_eq!(name, "MyTerm");
4142 }
4143 _ => panic!("Expected NewConsole variant with name"),
4144 }
4145
4146 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
4148 SerialConfigCli::NewConsole(Some(path), None) => {
4149 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
4150 }
4151 _ => panic!("Expected NewConsole variant without name"),
4152 }
4153
4154 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
4156 SerialConfigCli::Tcp(addr) => {
4157 assert_eq!(addr.to_string(), "127.0.0.1:1234");
4158 }
4159 _ => panic!("Expected Tcp variant"),
4160 }
4161
4162 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
4164 SerialConfigCli::Pipe(path) => {
4165 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
4166 }
4167 _ => panic!("Expected Pipe variant"),
4168 }
4169
4170 assert!(SerialConfigCli::from_str("").is_err());
4172 assert!(SerialConfigCli::from_str("unknown").is_err());
4173 assert!(SerialConfigCli::from_str("file").is_err());
4174 assert!(SerialConfigCli::from_str("listen").is_err());
4175 }
4176
4177 #[test]
4178 fn test_endpoint_config_from_str() {
4179 assert!(matches!(
4181 EndpointConfigCli::from_str("none").unwrap(),
4182 EndpointConfigCli::None
4183 ));
4184
4185 match EndpointConfigCli::from_str("consomme").unwrap() {
4187 EndpointConfigCli::Consomme {
4188 cidr: None,
4189 host_fwd,
4190 } => assert!(host_fwd.is_empty()),
4191 _ => panic!("Expected Consomme variant without cidr"),
4192 }
4193
4194 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
4196 EndpointConfigCli::Consomme {
4197 cidr: Some(cidr),
4198 host_fwd,
4199 } => {
4200 assert_eq!(cidr, "192.168.0.0/24");
4201 assert!(host_fwd.is_empty());
4202 }
4203 _ => panic!("Expected Consomme variant with cidr"),
4204 }
4205
4206 match EndpointConfigCli::from_str("consomme:hostfwd=udp:127.0.0.1:5000-:5000").unwrap() {
4208 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4209 assert!(cidr.is_none());
4210 assert_eq!(host_fwd.len(), 1);
4211 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Udp);
4212 assert_eq!(
4213 host_fwd[0].host_address,
4214 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
4215 );
4216 assert_eq!(host_fwd[0].host_port, 5000);
4217 assert_eq!(host_fwd[0].guest_port, 5000);
4218 }
4219 _ => panic!("Expected Consomme variant with hostfwd"),
4220 }
4221
4222 match EndpointConfigCli::from_str("consomme:10.0.0.0/24,hostfwd=tcp::2222-:22").unwrap() {
4224 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4225 assert_eq!(cidr.as_deref(), Some("10.0.0.0/24"));
4226 assert_eq!(host_fwd.len(), 1);
4227 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4228 assert_eq!(host_fwd[0].host_port, 2222);
4229 assert_eq!(host_fwd[0].guest_port, 22);
4230 }
4231 _ => panic!("Expected Consomme variant with cidr and hostfwd"),
4232 }
4233
4234 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::2222-:22,hostfwd=tcp::3389-:3389")
4236 .unwrap()
4237 {
4238 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4239 assert!(cidr.is_none());
4240 assert_eq!(host_fwd.len(), 2);
4241 assert_eq!(host_fwd[0].host_port, 2222);
4242 assert_eq!(host_fwd[0].guest_port, 22);
4243 assert_eq!(host_fwd[1].host_port, 3389);
4244 assert_eq!(host_fwd[1].guest_port, 3389);
4245 }
4246 _ => panic!("Expected Consomme variant with multiple hostfwd"),
4247 }
4248
4249 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:127.0.0.1:8080-:80").unwrap() {
4251 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4252 assert!(cidr.is_none());
4253 assert_eq!(host_fwd.len(), 1);
4254 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4255 assert_eq!(
4256 host_fwd[0].host_address,
4257 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
4258 );
4259 assert_eq!(host_fwd[0].host_port, 8080);
4260 assert_eq!(host_fwd[0].guest_port, 80);
4261 }
4262 _ => panic!("Expected Consomme variant with host/guest port mapping"),
4263 }
4264
4265 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-10.0.0.2:80").unwrap() {
4267 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4268 assert!(cidr.is_none());
4269 assert_eq!(host_fwd[0].host_port, 8080);
4270 assert_eq!(host_fwd[0].guest_port, 80);
4271 }
4272 _ => panic!("Expected Consomme variant with guest address"),
4273 }
4274
4275 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:[::1]:8080-:80").unwrap() {
4277 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4278 assert!(cidr.is_none());
4279 assert_eq!(host_fwd.len(), 1);
4280 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4281 assert_eq!(
4282 host_fwd[0].host_address,
4283 Some(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST))
4284 );
4285 assert_eq!(host_fwd[0].host_port, 8080);
4286 assert_eq!(host_fwd[0].guest_port, 80);
4287 }
4288 _ => panic!("Expected Consomme variant with IPv6 hostfwd"),
4289 }
4290
4291 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-[::1]:80").unwrap() {
4293 EndpointConfigCli::Consomme { cidr, host_fwd } => {
4294 assert!(cidr.is_none());
4295 assert_eq!(host_fwd[0].host_port, 8080);
4296 assert_eq!(host_fwd[0].guest_port, 80);
4297 }
4298 _ => panic!("Expected Consomme variant with IPv6 guest address"),
4299 }
4300
4301 match EndpointConfigCli::from_str("dio").unwrap() {
4303 EndpointConfigCli::Dio { id: None } => (),
4304 _ => panic!("Expected Dio variant without id"),
4305 }
4306
4307 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
4309 EndpointConfigCli::Dio { id: Some(id) } => {
4310 assert_eq!(id, "test_id");
4311 }
4312 _ => panic!("Expected Dio variant with id"),
4313 }
4314
4315 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
4317 EndpointConfigCli::Tap { name } => {
4318 assert_eq!(name, "tap0");
4319 }
4320 _ => panic!("Expected Tap variant"),
4321 }
4322
4323 assert!(EndpointConfigCli::from_str("invalid").is_err());
4325 }
4326
4327 #[test]
4328 fn test_nic_config_from_str() {
4329 use openvmm_defs::config::DeviceVtl;
4330
4331 let config = NicConfigCli::from_str("none").unwrap();
4333 assert_eq!(config.vtl, DeviceVtl::Vtl0);
4334 assert!(config.max_queues.is_none());
4335 assert!(!config.underhill);
4336 assert!(config.pcie_port.is_none());
4337 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4338
4339 let config = NicConfigCli::from_str("vtl2:none").unwrap();
4341 assert_eq!(config.vtl, DeviceVtl::Vtl2);
4342 assert!(config.pcie_port.is_none());
4343 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4344
4345 let config = NicConfigCli::from_str("queues=4:none").unwrap();
4347 assert_eq!(config.max_queues, Some(4));
4348 assert!(config.pcie_port.is_none());
4349 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4350
4351 let config = NicConfigCli::from_str("uh:none").unwrap();
4353 assert!(config.underhill);
4354 assert!(config.pcie_port.is_none());
4355 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4356
4357 let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
4359 assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
4360 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4361
4362 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
4364 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
4366 assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
4367 assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
4368 assert!(NicConfigCli::from_str("pcie_port:none").is_err());
4369 }
4370
4371 #[test]
4372 fn test_parse_pcie_port_prefix() {
4373 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
4375 assert_eq!(port.unwrap(), "rp0");
4376 assert_eq!(rest, "tag,path");
4377
4378 let (port, rest) = parse_pcie_port_prefix("tag,path");
4380 assert!(port.is_none());
4381 assert_eq!(rest, "tag,path");
4382
4383 let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
4385 assert!(port.is_none());
4386 assert_eq!(rest, "pcie_port=:tag,path");
4387
4388 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
4390 assert!(port.is_none());
4391 assert_eq!(rest, "pcie_port=rp0");
4392 }
4393
4394 #[test]
4395 fn test_cxl_test_device_cli_parse_valid() {
4396 let cfg = CxlTestDeviceCli::from_str("mem:1G,pcie_port=rp0").unwrap();
4397 assert_eq!(cfg.hdm_size, 1024 * 1024 * 1024);
4398 assert_eq!(cfg.pcie_port, "rp0");
4399 }
4400
4401 #[test]
4402 fn test_cxl_test_device_cli_parse_invalid() {
4403 assert!(CxlTestDeviceCli::from_str("file:disk.img,pcie_port=rp0").is_err());
4404 assert!(CxlTestDeviceCli::from_str("mem:1G").is_err());
4405 assert!(CxlTestDeviceCli::from_str("mem:1G,pcie_port=").is_err());
4406 }
4407
4408 #[test]
4409 fn test_fs_args_pcie_port() {
4410 let args = FsArgs::from_str("myfs,/path").unwrap();
4412 assert_eq!(args.tag, "myfs");
4413 assert_eq!(args.path, "/path");
4414 assert!(args.pcie_port.is_none());
4415
4416 let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
4418 assert_eq!(args.pcie_port.unwrap(), "rp0");
4419 assert_eq!(args.tag, "myfs");
4420 assert_eq!(args.path, "/path");
4421
4422 assert!(FsArgs::from_str("myfs").is_err());
4424 assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
4425 }
4426
4427 #[test]
4428 fn test_fs_args_with_options_pcie_port() {
4429 let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
4431 assert_eq!(args.tag, "myfs");
4432 assert_eq!(args.path, "/path");
4433 assert_eq!(args.options, "uid=1000");
4434 assert!(args.pcie_port.is_none());
4435
4436 let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
4438 assert_eq!(args.pcie_port.unwrap(), "rp0");
4439 assert_eq!(args.tag, "myfs");
4440 assert_eq!(args.path, "/path");
4441 assert_eq!(args.options, "uid=1000");
4442
4443 assert!(FsArgsWithOptions::from_str("myfs").is_err());
4445 }
4446
4447 #[test]
4448 fn test_virtio_pmem_args_pcie_port() {
4449 let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
4451 assert_eq!(args.path, "/path/to/file");
4452 assert!(args.pcie_port.is_none());
4453
4454 let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
4456 assert_eq!(args.pcie_port.unwrap(), "rp0");
4457 assert_eq!(args.path, "/path/to/file");
4458
4459 assert!(VirtioPmemArgs::from_str("").is_err());
4461 assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
4462 }
4463
4464 #[test]
4465 fn test_smt_config_from_str() {
4466 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
4467 assert_eq!(
4468 SmtConfigCli::from_str("force").unwrap(),
4469 SmtConfigCli::Force
4470 );
4471 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
4472
4473 assert!(SmtConfigCli::from_str("invalid").is_err());
4475 assert!(SmtConfigCli::from_str("").is_err());
4476 }
4477
4478 #[test]
4479 fn test_pcat_boot_order_from_str() {
4480 let order = PcatBootOrderCli::from_str("optical").unwrap();
4482 assert_eq!(order.0[0], PcatBootDevice::Optical);
4483
4484 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
4486 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
4487 assert_eq!(order.0[1], PcatBootDevice::Network);
4488
4489 assert!(PcatBootOrderCli::from_str("invalid").is_err());
4491 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
4493
4494 #[test]
4495 fn test_floppy_disk_from_str() {
4496 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
4498 assert!(!disk.read_only);
4499 match disk.kind {
4500 DiskCliKind::File {
4501 path,
4502 create_with_len,
4503 ..
4504 } => {
4505 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
4506 assert_eq!(create_with_len, None);
4507 }
4508 _ => panic!("Expected File variant"),
4509 }
4510
4511 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
4513 assert!(disk.read_only);
4514
4515 assert!(FloppyDiskCli::from_str("").is_err());
4517 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
4518 }
4519
4520 #[test]
4521 fn test_pcie_root_complex_from_str() {
4522 const ONE_MB: u64 = 1024 * 1024;
4523 const ONE_GB: u64 = 1024 * ONE_MB;
4524
4525 const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
4526 const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
4527 const DEFAULT_HDM: u64 = ONE_GB;
4528 const DEFAULT_HDM_WINDOW_RESTRICTIONS: CfmwsWindowRestrictions =
4529 CfmwsWindowRestrictions::DEVICE_COHERENT;
4530
4531 assert_eq!(
4532 PcieRootComplexCli::from_str("rc0").unwrap(),
4533 PcieRootComplexCli {
4534 name: "rc0".to_string(),
4535 segment: 0,
4536 start_bus: 0,
4537 end_bus: 255,
4538 low_mmio: DEFAULT_LOW_MMIO,
4539 high_mmio: DEFAULT_HIGH_MMIO,
4540 hdm: DEFAULT_HDM,
4541 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4542 vnode: None,
4543 low_mmio_base: None,
4544 high_mmio_base: None,
4545 preserve_bars: false,
4546 }
4547 );
4548
4549 assert_eq!(
4550 PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
4551 PcieRootComplexCli {
4552 name: "rc1".to_string(),
4553 segment: 1,
4554 start_bus: 0,
4555 end_bus: 255,
4556 low_mmio: DEFAULT_LOW_MMIO,
4557 high_mmio: DEFAULT_HIGH_MMIO,
4558 hdm: DEFAULT_HDM,
4559 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4560 vnode: None,
4561 low_mmio_base: None,
4562 high_mmio_base: None,
4563 preserve_bars: false,
4564 }
4565 );
4566
4567 assert_eq!(
4568 PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
4569 PcieRootComplexCli {
4570 name: "rc2".to_string(),
4571 segment: 0,
4572 start_bus: 32,
4573 end_bus: 255,
4574 low_mmio: DEFAULT_LOW_MMIO,
4575 high_mmio: DEFAULT_HIGH_MMIO,
4576 hdm: DEFAULT_HDM,
4577 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4578 vnode: None,
4579 low_mmio_base: None,
4580 high_mmio_base: None,
4581 preserve_bars: false,
4582 }
4583 );
4584
4585 assert_eq!(
4586 PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
4587 PcieRootComplexCli {
4588 name: "rc3".to_string(),
4589 segment: 0,
4590 start_bus: 0,
4591 end_bus: 31,
4592 low_mmio: DEFAULT_LOW_MMIO,
4593 high_mmio: DEFAULT_HIGH_MMIO,
4594 hdm: DEFAULT_HDM,
4595 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4596 vnode: None,
4597 low_mmio_base: None,
4598 high_mmio_base: None,
4599 preserve_bars: false,
4600 }
4601 );
4602
4603 assert_eq!(
4604 PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
4605 PcieRootComplexCli {
4606 name: "rc4".to_string(),
4607 segment: 0,
4608 start_bus: 32,
4609 end_bus: 127,
4610 low_mmio: DEFAULT_LOW_MMIO,
4611 high_mmio: 2 * ONE_GB,
4612 hdm: DEFAULT_HDM,
4613 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4614 vnode: None,
4615 low_mmio_base: None,
4616 high_mmio_base: None,
4617 preserve_bars: false,
4618 }
4619 );
4620
4621 assert_eq!(
4622 PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
4623 PcieRootComplexCli {
4624 name: "rc5".to_string(),
4625 segment: 2,
4626 start_bus: 32,
4627 end_bus: 127,
4628 low_mmio: DEFAULT_LOW_MMIO,
4629 high_mmio: DEFAULT_HIGH_MMIO,
4630 hdm: DEFAULT_HDM,
4631 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4632 vnode: None,
4633 low_mmio_base: None,
4634 high_mmio_base: None,
4635 preserve_bars: false,
4636 }
4637 );
4638
4639 assert_eq!(
4640 PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
4641 PcieRootComplexCli {
4642 name: "rc6".to_string(),
4643 segment: 0,
4644 start_bus: 0,
4645 end_bus: 255,
4646 low_mmio: ONE_MB as u32,
4647 high_mmio: 64 * ONE_GB,
4648 hdm: DEFAULT_HDM,
4649 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4650 vnode: None,
4651 low_mmio_base: None,
4652 high_mmio_base: None,
4653 preserve_bars: false,
4654 }
4655 );
4656
4657 assert_eq!(
4658 PcieRootComplexCli::from_str("rc7,hdm=2G").unwrap(),
4659 PcieRootComplexCli {
4660 name: "rc7".to_string(),
4661 segment: 0,
4662 start_bus: 0,
4663 end_bus: 255,
4664 low_mmio: DEFAULT_LOW_MMIO,
4665 high_mmio: DEFAULT_HIGH_MMIO,
4666 hdm: 2 * ONE_GB,
4667 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4668 vnode: None,
4669 low_mmio_base: None,
4670 high_mmio_base: None,
4671 preserve_bars: false,
4672 }
4673 );
4674
4675 assert_eq!(
4676 PcieRootComplexCli::from_str("rc8,hdm_window_restrictions=0x21").unwrap(),
4677 PcieRootComplexCli {
4678 name: "rc8".to_string(),
4679 segment: 0,
4680 start_bus: 0,
4681 end_bus: 255,
4682 low_mmio: DEFAULT_LOW_MMIO,
4683 high_mmio: DEFAULT_HIGH_MMIO,
4684 hdm: DEFAULT_HDM,
4685 hdm_window_restrictions: CfmwsWindowRestrictions::try_from_bits(0x21).unwrap(),
4686 vnode: None,
4687 low_mmio_base: None,
4688 high_mmio_base: None,
4689 preserve_bars: false,
4690 }
4691 );
4692
4693 assert!(PcieRootComplexCli::from_str("").is_err());
4695 assert!(PcieRootComplexCli::from_str("poorly,").is_err());
4696 assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
4697 assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
4698 assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
4699 assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
4700 assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
4701 assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
4702 assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
4703 assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
4704 assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
4705 assert!(PcieRootComplexCli::from_str("rc,hdm=bad").is_err());
4706 assert!(PcieRootComplexCli::from_str("rc,hdm").is_err());
4707 assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions=bad").is_err());
4708 assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions").is_err());
4709 assert!(PcieRootComplexCli::from_str("rc,cxl").is_err());
4710
4711 assert_eq!(
4713 PcieRootComplexCli::from_str("rc9,node=1").unwrap(),
4714 PcieRootComplexCli {
4715 name: "rc9".to_string(),
4716 segment: 0,
4717 start_bus: 0,
4718 end_bus: 255,
4719 low_mmio: DEFAULT_LOW_MMIO,
4720 high_mmio: DEFAULT_HIGH_MMIO,
4721 hdm: DEFAULT_HDM,
4722 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4723 vnode: Some(1),
4724 low_mmio_base: None,
4725 high_mmio_base: None,
4726 preserve_bars: false,
4727 }
4728 );
4729 }
4730
4731 #[test]
4732 fn test_pcie_root_port_from_str() {
4733 assert_eq!(
4734 PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
4735 PcieRootPortCli {
4736 root_complex_name: "rc0".to_string(),
4737 name: "rc0rp0".to_string(),
4738 devfn: None,
4739 hotplug: false,
4740 acs_capabilities_supported: None,
4741 cxl: false,
4742 pasid: false,
4743 }
4744 );
4745
4746 assert_eq!(
4747 PcieRootPortCli::from_str("my_rc:port2").unwrap(),
4748 PcieRootPortCli {
4749 root_complex_name: "my_rc".to_string(),
4750 name: "port2".to_string(),
4751 devfn: None,
4752 hotplug: false,
4753 acs_capabilities_supported: None,
4754 cxl: false,
4755 pasid: false,
4756 }
4757 );
4758
4759 assert_eq!(
4761 PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
4762 PcieRootPortCli {
4763 root_complex_name: "my_rc".to_string(),
4764 name: "port2".to_string(),
4765 devfn: None,
4766 hotplug: true,
4767 acs_capabilities_supported: None,
4768 cxl: false,
4769 pasid: false,
4770 }
4771 );
4772
4773 assert_eq!(
4774 PcieRootPortCli::from_str("my_rc:port3,acs=0").unwrap(),
4775 PcieRootPortCli {
4776 root_complex_name: "my_rc".to_string(),
4777 name: "port3".to_string(),
4778 devfn: None,
4779 hotplug: false,
4780 acs_capabilities_supported: Some(0),
4781 cxl: false,
4782 pasid: false,
4783 }
4784 );
4785
4786 assert_eq!(
4787 PcieRootPortCli::from_str("my_rc:port3,acs=0x5f").unwrap(),
4788 PcieRootPortCli {
4789 root_complex_name: "my_rc".to_string(),
4790 name: "port3".to_string(),
4791 devfn: None,
4792 hotplug: false,
4793 acs_capabilities_supported: Some(0x005f),
4794 cxl: false,
4795 pasid: false,
4796 }
4797 );
4798
4799 assert_eq!(
4800 PcieRootPortCli::from_str("my_rc:port4,cxl").unwrap(),
4801 PcieRootPortCli {
4802 root_complex_name: "my_rc".to_string(),
4803 name: "port4".to_string(),
4804 devfn: None,
4805 hotplug: false,
4806 acs_capabilities_supported: None,
4807 cxl: true,
4808 pasid: false,
4809 }
4810 );
4811
4812 assert_eq!(
4814 PcieRootPortCli::from_str("my_rc:port5,addr=5").unwrap(),
4815 PcieRootPortCli {
4816 root_complex_name: "my_rc".to_string(),
4817 name: "port5".to_string(),
4818 devfn: Some(5 << 3),
4819 hotplug: false,
4820 acs_capabilities_supported: None,
4821 cxl: false,
4822 pasid: false,
4823 }
4824 );
4825 assert_eq!(
4826 PcieRootPortCli::from_str("my_rc:port6,addr=5.1").unwrap(),
4827 PcieRootPortCli {
4828 root_complex_name: "my_rc".to_string(),
4829 name: "port6".to_string(),
4830 devfn: Some((5 << 3) | 1),
4831 hotplug: false,
4832 acs_capabilities_supported: None,
4833 cxl: false,
4834 pasid: false,
4835 }
4836 );
4837 assert_eq!(
4838 PcieRootPortCli::from_str("my_rc:port7,addr=0x1f.7").unwrap(),
4839 PcieRootPortCli {
4840 root_complex_name: "my_rc".to_string(),
4841 name: "port7".to_string(),
4842 devfn: Some(0xff),
4843 hotplug: false,
4844 acs_capabilities_supported: None,
4845 cxl: false,
4846 pasid: false,
4847 }
4848 );
4849
4850 assert_eq!(
4851 PcieRootPortCli::from_str("my_rc:port8,pasid").unwrap(),
4852 PcieRootPortCli {
4853 root_complex_name: "my_rc".to_string(),
4854 name: "port8".to_string(),
4855 devfn: None,
4856 hotplug: false,
4857 acs_capabilities_supported: None,
4858 cxl: false,
4859 pasid: true,
4860 }
4861 );
4862
4863 assert!(PcieRootPortCli::from_str("").is_err());
4865 assert!(PcieRootPortCli::from_str("rp0").is_err());
4866 assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
4867 assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
4868 assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
4869 assert!(PcieRootPortCli::from_str("rc0:rp0,cxl=true").is_err());
4870 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=32").is_err());
4871 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=0.8").is_err());
4872 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=1.2.3").is_err());
4873 assert!(PcieRootPortCli::from_str("rc0:rp0,addr").is_err());
4874 assert!(PcieRootPortCli::from_str("rc0:rp0,pasid=foo").is_err());
4875 }
4876
4877 #[test]
4878 fn test_pcie_generic_initiator_from_str() {
4879 assert_eq!(
4880 PcieGenericInitiatorCli::from_str("port=rp0,node=1").unwrap(),
4881 PcieGenericInitiatorCli {
4882 port_name: "rp0".to_string(),
4883 node: 1,
4884 }
4885 );
4886
4887 assert_eq!(
4889 PcieGenericInitiatorCli::from_str("node=2,port=sw0-downstream-1").unwrap(),
4890 PcieGenericInitiatorCli {
4891 port_name: "sw0-downstream-1".to_string(),
4892 node: 2,
4893 }
4894 );
4895
4896 assert!(PcieGenericInitiatorCli::from_str("").is_err());
4898 assert!(PcieGenericInitiatorCli::from_str("port=rp0").is_err());
4899 assert!(PcieGenericInitiatorCli::from_str("node=1").is_err());
4900 assert!(PcieGenericInitiatorCli::from_str("rp0=1").is_err());
4901 assert!(PcieGenericInitiatorCli::from_str("port=,node=1").is_err());
4902 assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=x").is_err());
4903 assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=1,extra").is_err());
4904 }
4905
4906 #[test]
4907 fn test_pcie_switch_from_str() {
4908 assert_eq!(
4909 GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
4910 GenericPcieSwitchCli {
4911 port_name: "rp0".to_string(),
4912 name: "switch0".to_string(),
4913 num_downstream_ports: 4,
4914 hotplug: false,
4915 acs_capabilities_supported: None,
4916 pasid: false,
4917 }
4918 );
4919
4920 assert_eq!(
4921 GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
4922 GenericPcieSwitchCli {
4923 port_name: "port1".to_string(),
4924 name: "my_switch".to_string(),
4925 num_downstream_ports: 4,
4926 hotplug: false,
4927 acs_capabilities_supported: None,
4928 pasid: false,
4929 }
4930 );
4931
4932 assert_eq!(
4933 GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
4934 GenericPcieSwitchCli {
4935 port_name: "rp2".to_string(),
4936 name: "sw".to_string(),
4937 num_downstream_ports: 8,
4938 hotplug: false,
4939 acs_capabilities_supported: None,
4940 pasid: false,
4941 }
4942 );
4943
4944 assert_eq!(
4946 GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
4947 GenericPcieSwitchCli {
4948 port_name: "switch0-downstream-1".to_string(),
4949 name: "child_switch".to_string(),
4950 num_downstream_ports: 4,
4951 hotplug: false,
4952 acs_capabilities_supported: None,
4953 pasid: false,
4954 }
4955 );
4956
4957 assert_eq!(
4959 GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
4960 GenericPcieSwitchCli {
4961 port_name: "rp0".to_string(),
4962 name: "switch0".to_string(),
4963 num_downstream_ports: 4,
4964 hotplug: true,
4965 acs_capabilities_supported: None,
4966 pasid: false,
4967 }
4968 );
4969
4970 assert_eq!(
4972 GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
4973 GenericPcieSwitchCli {
4974 port_name: "rp0".to_string(),
4975 name: "switch0".to_string(),
4976 num_downstream_ports: 8,
4977 hotplug: true,
4978 acs_capabilities_supported: None,
4979 pasid: false,
4980 }
4981 );
4982
4983 assert_eq!(
4984 GenericPcieSwitchCli::from_str("rp0:switch0,acs=0").unwrap(),
4985 GenericPcieSwitchCli {
4986 port_name: "rp0".to_string(),
4987 name: "switch0".to_string(),
4988 num_downstream_ports: 4,
4989 hotplug: false,
4990 acs_capabilities_supported: Some(0),
4991 pasid: false,
4992 }
4993 );
4994
4995 assert_eq!(
4996 GenericPcieSwitchCli::from_str("rp0:switch0,acs=95").unwrap(),
4997 GenericPcieSwitchCli {
4998 port_name: "rp0".to_string(),
4999 name: "switch0".to_string(),
5000 num_downstream_ports: 4,
5001 hotplug: false,
5002 acs_capabilities_supported: Some(95),
5003 pasid: false,
5004 }
5005 );
5006
5007 assert_eq!(
5008 GenericPcieSwitchCli::from_str("rp0:switch0,pasid").unwrap(),
5009 GenericPcieSwitchCli {
5010 port_name: "rp0".to_string(),
5011 name: "switch0".to_string(),
5012 num_downstream_ports: 4,
5013 hotplug: false,
5014 acs_capabilities_supported: None,
5015 pasid: true,
5016 }
5017 );
5018
5019 assert!(GenericPcieSwitchCli::from_str("").is_err());
5021 assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
5022 assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
5023 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
5024 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
5025 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
5026 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
5027 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,pasid=bar").is_err());
5028 }
5029
5030 #[test]
5031 fn test_pcie_remote_from_str() {
5032 assert_eq!(
5034 PcieRemoteCli::from_str("rc0rp0").unwrap(),
5035 PcieRemoteCli {
5036 port_name: "rc0rp0".to_string(),
5037 socket_addr: None,
5038 hu: 0,
5039 controller: 0,
5040 }
5041 );
5042
5043 assert_eq!(
5045 PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
5046 PcieRemoteCli {
5047 port_name: "rc0rp0".to_string(),
5048 socket_addr: Some("localhost:22567".to_string()),
5049 hu: 0,
5050 controller: 0,
5051 }
5052 );
5053
5054 assert_eq!(
5056 PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
5057 PcieRemoteCli {
5058 port_name: "myport".to_string(),
5059 socket_addr: Some("localhost:22568".to_string()),
5060 hu: 1,
5061 controller: 2,
5062 }
5063 );
5064
5065 assert_eq!(
5067 PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
5068 PcieRemoteCli {
5069 port_name: "port0".to_string(),
5070 socket_addr: None,
5071 hu: 5,
5072 controller: 3,
5073 }
5074 );
5075
5076 assert!(PcieRemoteCli::from_str("").is_err());
5078 assert!(PcieRemoteCli::from_str("port,socket=").is_err());
5079 assert!(PcieRemoteCli::from_str("port,hu=").is_err());
5080 assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
5081 assert!(PcieRemoteCli::from_str("port,controller=").is_err());
5082 assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
5083 assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
5084 }
5085
5086 #[test]
5087 fn test_parse_memory_units() {
5088 assert_eq!(parse_memory("64G").unwrap(), 64 * 1024 * 1024 * 1024);
5089 assert_eq!(parse_memory("64GB").unwrap(), 64 * 1024 * 1024 * 1024);
5090 assert_eq!(parse_memory("3MB").unwrap(), 3 * 1024 * 1024);
5091 assert_eq!(parse_memory("512KB").unwrap(), 512 * 1024);
5092 assert!(parse_memory("3MiB").is_err());
5093 }
5094
5095 #[test]
5096 fn test_memory_config_size_only() {
5097 assert_eq!(
5098 parse_memory_config("64G").unwrap(),
5099 MemoryCli {
5100 size: Some(vmm_cli::MemorySize(64 * 1024 * 1024 * 1024)),
5101 ..Default::default()
5102 }
5103 );
5104 }
5105
5106 #[test]
5107 fn test_memory_config_key_value() {
5108 assert_eq!(
5109 parse_memory_config("size=2G,shared=off,prefetch=on,thp=on").unwrap(),
5110 MemoryCli {
5111 size: Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024)),
5112 shared: Some(false),
5113 prefetch: true,
5114 transparent_hugepages: Some(true),
5115 ..Default::default()
5116 }
5117 );
5118
5119 assert_eq!(
5120 parse_memory_config("size=4GB,hugepages=on,hugepage_size=2MB").unwrap(),
5121 MemoryCli {
5122 size: Some(vmm_cli::MemorySize(4 * 1024 * 1024 * 1024)),
5123 hugepages: true,
5124 hugepage_size: Some(vmm_cli::MemorySize(2 * 1024 * 1024)),
5125 ..Default::default()
5126 }
5127 );
5128
5129 assert_eq!(
5130 parse_memory_config("file=/tmp/memory.bin").unwrap(),
5131 MemoryCli {
5132 file: Some(PathBuf::from("/tmp/memory.bin")),
5133 ..Default::default()
5134 }
5135 );
5136 }
5137
5138 #[test]
5139 fn test_memory_config_rejects_invalid_combinations() {
5140 assert!(parse_memory_config("size=1G,size=2G").is_err());
5141 assert!(parse_memory_config("hugepage_size=2M").is_err());
5142 assert!(parse_memory_config("hugepages=on,shared=off").is_err());
5143 assert!(parse_memory_config("hugepages=on,file=/tmp/memory.bin").is_err());
5144
5145 assert_eq!(
5148 parse_memory_config("hugepages=on,hugepage_size=3MB")
5149 .unwrap()
5150 .hugepage_size,
5151 Some(vmm_cli::MemorySize(3 * 1024 * 1024))
5152 );
5153 }
5154
5155 #[test]
5156 fn test_smbios_requires_type() {
5157 assert!(parse_smbios("manufacturer=Contoso,family=Foo").is_err());
5159 }
5160
5161 #[test]
5162 fn test_smbios_all_system_keys() {
5163 let parsed = parse_smbios(
5164 "type=1,manufacturer=M,product=P,version=V,serial=S,sku=K,family=F,\
5165 uuid=12345678-9abc-def0-1234-56789abcdef0",
5166 )
5167 .unwrap();
5168 assert_eq!(
5169 parsed,
5170 SmbiosCli {
5171 system: SmbiosSystemCli {
5172 manufacturer: Some("M".to_string()),
5173 product_name: Some("P".to_string()),
5174 version: Some("V".to_string()),
5175 serial_number: Some("S".to_string()),
5176 sku_number: Some("K".to_string()),
5177 family: Some("F".to_string()),
5178 uuid: Some(SmbiosUuid::Fixed(
5179 "12345678-9abc-def0-1234-56789abcdef0"
5180 .parse::<Guid>()
5181 .unwrap()
5182 )),
5183 },
5184 ..Default::default()
5185 }
5186 );
5187 }
5188
5189 #[test]
5190 fn test_smbios_bios_keys() {
5191 let parsed =
5192 parse_smbios("type=0,vendor=Contoso,version=1.0,date=01/01/2026,release=4.1").unwrap();
5193 assert_eq!(
5194 parsed,
5195 SmbiosCli {
5196 bios: SmbiosBiosCli {
5197 vendor: Some("Contoso".to_string()),
5198 version: Some("1.0".to_string()),
5199 release_date: Some("01/01/2026".to_string()),
5200 release: Some(SmbiosRelease(4, 1)),
5201 },
5202 ..Default::default()
5203 }
5204 );
5205 }
5206
5207 #[test]
5208 fn test_smbios_release_parsing() {
5209 assert_eq!(
5211 parse_smbios("type=0,release=4.1").unwrap().bios.release,
5212 Some(SmbiosRelease(4, 1))
5213 );
5214 assert_eq!(
5215 parse_smbios("type=0,release=255.0").unwrap().bios.release,
5216 Some(SmbiosRelease(255, 0))
5217 );
5218 assert!(parse_smbios("type=0,release=4").is_err());
5221 assert!(parse_smbios("type=0,release=4.1.0").is_err());
5222 assert!(parse_smbios("type=0,release=256.0").is_err());
5223 assert!(parse_smbios("type=0,release=x.y").is_err());
5224 assert!(parse_smbios("type=1,release=4.1").is_err());
5226 }
5227
5228 #[test]
5229 fn test_smbios_uuid() {
5230 assert_eq!(
5232 parse_smbios("type=1,uuid=12345678-9abc-def0-1234-56789abcdef0")
5233 .unwrap()
5234 .system
5235 .uuid,
5236 Some(SmbiosUuid::Fixed(
5237 "12345678-9abc-def0-1234-56789abcdef0"
5238 .parse::<Guid>()
5239 .unwrap()
5240 ))
5241 );
5242 assert_eq!(
5244 parse_smbios("type=1,uuid=random").unwrap().system.uuid,
5245 Some(SmbiosUuid::Random)
5246 );
5247 }
5248
5249 #[test]
5250 fn test_smbios_version_disambiguated_by_type() {
5251 let bios = parse_smbios("type=0,version=1.0").unwrap();
5253 assert_eq!(bios.bios.version.as_deref(), Some("1.0"));
5254 assert_eq!(bios.system.version, None);
5255
5256 let system = parse_smbios("type=1,version=2.0").unwrap();
5257 assert_eq!(system.system.version.as_deref(), Some("2.0"));
5258 assert_eq!(system.bios.version, None);
5259 }
5260
5261 #[test]
5262 fn test_smbios_rejects_invalid() {
5263 assert!(parse_smbios("manufacturer=M").is_err());
5265 assert!(parse_smbios("type=0,manufacturer=M").is_err());
5267 assert!(parse_smbios("type=1,nonsense=x").is_err());
5268 assert!(parse_smbios("type=2,vendor=M").is_err());
5270 assert!(parse_smbios("type=bad,vendor=M").is_err());
5271 assert!(parse_smbios("type=1,manufacturer").is_err());
5273 assert!(parse_smbios("=value").is_err());
5274 assert!(parse_smbios("type=1,manufacturer=").is_err());
5275 assert!(parse_smbios("type=1,manufacturer=A,manufacturer=B").is_err());
5277 assert!(parse_smbios("type=0,type=1,vendor=M").is_err());
5278 assert!(parse_smbios("type=1,uuid=not-a-guid").is_err());
5280 }
5281
5282 #[test]
5283 fn test_smbios_merge_across_arguments() {
5284 let mut merged = SmbiosCli::default();
5285 merged
5286 .merge(parse_smbios("type=0,vendor=Contoso").unwrap())
5287 .unwrap();
5288 merged
5289 .merge(parse_smbios("type=1,manufacturer=M,family=F").unwrap())
5290 .unwrap();
5291 assert_eq!(
5292 merged,
5293 SmbiosCli {
5294 bios: SmbiosBiosCli {
5295 vendor: Some("Contoso".to_string()),
5296 ..Default::default()
5297 },
5298 system: SmbiosSystemCli {
5299 manufacturer: Some("M".to_string()),
5300 family: Some("F".to_string()),
5301 ..Default::default()
5302 },
5303 }
5304 );
5305 }
5306
5307 #[test]
5308 fn test_smbios_merge_rejects_conflicts() {
5309 let mut merged = SmbiosCli::default();
5310 merged
5311 .merge(parse_smbios("type=1,manufacturer=A").unwrap())
5312 .unwrap();
5313 assert!(
5315 merged
5316 .merge(parse_smbios("type=1,manufacturer=B").unwrap())
5317 .is_err()
5318 );
5319 }
5320
5321 #[test]
5322 fn test_smbios_arg_repeatable() {
5323 let opt = Options::try_parse_from([
5324 "openvmm",
5325 "--smbios",
5326 "type=1,manufacturer=Contoso",
5327 "--smbios",
5328 "type=0,vendor=Acme",
5329 ])
5330 .unwrap();
5331 assert_eq!(opt.smbios.len(), 2);
5332 assert_eq!(
5333 opt.smbios[0].system.manufacturer.as_deref(),
5334 Some("Contoso")
5335 );
5336 assert_eq!(opt.smbios[1].bios.vendor.as_deref(), Some("Acme"));
5337 }
5338
5339 #[test]
5340 fn test_memory_options_merge_legacy_aliases() {
5341 let opt = Options::try_parse_from([
5342 "openvmm",
5343 "--memory",
5344 "2G",
5345 "--prefetch",
5346 "--private-memory",
5347 "--thp",
5348 ])
5349 .unwrap();
5350 opt.validate_memory_options().unwrap();
5351 assert_eq!(opt.memory_size(), 2 * 1024 * 1024 * 1024);
5352 assert!(opt.prefetch_memory());
5353 assert!(opt.private_memory());
5354 assert!(opt.transparent_hugepages());
5355 }
5356
5357 #[test]
5358 fn test_serial_debugger_mode_option_parsed() {
5359 let opt = Options::try_parse_from(["openvmm"]).unwrap();
5361 assert!(opt.com1.is_none());
5362
5363 let opt = Options::try_parse_from(["openvmm", "--com1", "none"]).unwrap();
5365 let com1 = opt.com1.unwrap();
5366 assert!(!com1.debugger_mode);
5367 assert_eq!(com1.backend, SerialConfigCli::None);
5368
5369 let opt = Options::try_parse_from([
5372 "openvmm",
5373 "--com1",
5374 "debugger-mode:listen=/tmp/kd",
5375 "--com2",
5376 "none",
5377 ])
5378 .unwrap();
5379 let com1 = opt.com1.unwrap();
5380 assert!(com1.debugger_mode);
5381 assert_eq!(com1.backend, SerialConfigCli::Pipe("/tmp/kd".into()));
5382 assert!(!opt.com2.unwrap().debugger_mode);
5384
5385 let opt = Options::try_parse_from([
5387 "openvmm",
5388 "--com1",
5389 "debugger-mode:listen=tcp:127.0.0.1:5555",
5390 ])
5391 .unwrap();
5392 let com1 = opt.com1.unwrap();
5393 assert!(com1.debugger_mode);
5394 assert_eq!(
5395 com1.backend,
5396 SerialConfigCli::Tcp("127.0.0.1:5555".parse().unwrap())
5397 );
5398 }
5399
5400 #[test]
5401 fn test_no_hv_requires_no_vmbus() {
5402 assert!(Options::try_parse_from(["openvmm", "--no-hv"]).is_err());
5403
5404 let opt = Options::try_parse_from(["openvmm", "--uefi", "--no-hv", "--no-vmbus"]).unwrap();
5405 assert!(opt.no_hv);
5406 assert!(opt.no_vmbus);
5407
5408 assert!(
5409 Options::try_parse_from(["openvmm", "--uefi", "--no-hv", "--no-vmbus", "--hv"])
5410 .is_err()
5411 );
5412 }
5413
5414 #[test]
5415 fn test_memory_options_allow_legacy_thp_with_new_private_memory() {
5416 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=off", "--thp"]).unwrap();
5417 opt.validate_memory_options().unwrap();
5418 assert!(opt.private_memory());
5419 assert!(opt.transparent_hugepages());
5420 }
5421
5422 #[test]
5423 fn test_memory_options_reject_conflicting_legacy_aliases() {
5424 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=on", "--private-memory"])
5425 .unwrap();
5426 assert!(opt.validate_memory_options().is_err());
5427 }
5428
5429 #[test]
5430 fn test_isolation_options_reject_snp_uefi() {
5431 let opt = Options::try_parse_from(["openvmm", "--isolation", "snp", "--uefi"]).unwrap();
5432
5433 assert_eq!(
5434 opt.validate_isolation_options().unwrap_err().to_string(),
5435 "SNP isolation currently only supports Linux direct boot"
5436 );
5437 }
5438
5439 #[test]
5440 fn test_isolation_options_reject_snp_hugepages() {
5441 for args in [
5442 vec![
5443 "openvmm",
5444 "--isolation",
5445 "snp",
5446 "--memory",
5447 "size=1G,hugepages=on",
5448 ],
5449 vec![
5450 "openvmm",
5451 "--isolation",
5452 "snp",
5453 "--numa",
5454 "size=1G,hugepages=on",
5455 ],
5456 ] {
5457 let opt = Options::try_parse_from(args).unwrap();
5458 assert_eq!(
5459 opt.validate_isolation_options().unwrap_err().to_string(),
5460 "SNP isolation currently does not support hugetlb memory"
5461 );
5462 }
5463 }
5464
5465 #[test]
5466 fn test_restricted_injection_requires_snp() {
5467 let opt = Options::try_parse_from(["openvmm", "--snp-restricted-injection"]).unwrap();
5468
5469 assert_eq!(
5470 opt.validate_isolation_options().unwrap_err().to_string(),
5471 "--snp-restricted-injection requires --isolation snp"
5472 );
5473
5474 let opt = Options::try_parse_from([
5475 "openvmm",
5476 "--isolation",
5477 "snp",
5478 "--snp-restricted-injection",
5479 ])
5480 .unwrap();
5481 assert_eq!(
5482 opt.validate_isolation_options().unwrap_err().to_string(),
5483 "--snp-restricted-injection requires --hypervisor mshv"
5484 );
5485
5486 let opt = Options::try_parse_from([
5487 "openvmm",
5488 "--hypervisor",
5489 "mshv",
5490 "--isolation",
5491 "snp",
5492 "--snp-restricted-injection",
5493 ])
5494 .unwrap();
5495 opt.validate_isolation_options().unwrap();
5496
5497 let opt = Options::try_parse_from([
5498 "openvmm",
5499 "--hypervisor",
5500 "mshv",
5501 "--isolation",
5502 "snp",
5503 "--snp-restricted-injection",
5504 "--pcat",
5505 ])
5506 .unwrap();
5507 assert_eq!(
5508 opt.validate_isolation_options().unwrap_err().to_string(),
5509 "--snp-restricted-injection requires Linux direct boot"
5510 );
5511
5512 let opt = Options::try_parse_from([
5513 "openvmm",
5514 "--hypervisor",
5515 "mshv",
5516 "--isolation",
5517 "snp",
5518 "--snp-restricted-injection",
5519 "--restore-snapshot",
5520 "snapshot",
5521 ])
5522 .unwrap();
5523 assert_eq!(
5524 opt.validate_isolation_options().unwrap_err().to_string(),
5525 "--snp-restricted-injection requires Linux direct boot"
5526 );
5527 }
5528
5529 #[test]
5530 fn test_isolation_options_allow_vbs_uefi() {
5531 let opt = Options::try_parse_from(["openvmm", "--isolation", "vbs", "--uefi"]).unwrap();
5532
5533 opt.validate_isolation_options().unwrap();
5534 }
5535
5536 #[test]
5537 fn test_isolation_options_allow_vbs_hugepages() {
5538 let opt = Options::try_parse_from([
5539 "openvmm",
5540 "--isolation",
5541 "vbs",
5542 "--memory",
5543 "size=1G,hugepages=on",
5544 ])
5545 .unwrap();
5546
5547 opt.validate_isolation_options().unwrap();
5548 }
5549
5550 #[test]
5551 fn test_igvm_personality_required_without_vtl2() {
5552 let opt = Options::try_parse_from(["openvmm", "--igvm", "guest.igvm"]).unwrap();
5553 assert_eq!(
5554 opt.validate_igvm_options().unwrap_err().to_string(),
5555 "--igvm-personality is required for non-VTL2 IGVM boots"
5556 );
5557 }
5558
5559 #[test]
5560 fn test_igvm_personality_values() {
5561 for (value, expected) in [
5562 ("uefi", IgvmPersonalityCli::Uefi),
5563 ("linux-direct", IgvmPersonalityCli::LinuxDirect),
5564 ] {
5565 let opt = Options::try_parse_from([
5566 "openvmm",
5567 "--igvm",
5568 "guest.igvm",
5569 "--igvm-personality",
5570 value,
5571 ])
5572 .unwrap();
5573 opt.validate_igvm_options().unwrap();
5574 assert_eq!(opt.igvm_personality, Some(expected));
5575 }
5576
5577 assert!(
5578 Options::try_parse_from([
5579 "openvmm",
5580 "--igvm",
5581 "guest.igvm",
5582 "--igvm-personality",
5583 "pcat",
5584 ])
5585 .is_err()
5586 );
5587 }
5588
5589 #[test]
5590 fn test_vtl2_igvm_keeps_implicit_hcl_personality() {
5591 let opt =
5592 Options::try_parse_from(["openvmm", "--igvm", "guest.igvm", "--hv", "--vtl2"]).unwrap();
5593 opt.validate_igvm_options().unwrap();
5594 assert_eq!(opt.igvm_personality, None);
5595 }
5596
5597 #[test]
5598 fn test_igvm_personality_conflicts_with_vtl2() {
5599 assert!(
5600 Options::try_parse_from([
5601 "openvmm",
5602 "--igvm",
5603 "guest.igvm",
5604 "--hv",
5605 "--vtl2",
5606 "--igvm-personality",
5607 "linux-direct",
5608 ])
5609 .is_err()
5610 );
5611 }
5612
5613 #[test]
5614 fn test_igvm_personality_conflicts_with_external_firmware() {
5615 for firmware in ["--uefi", "--pcat"] {
5616 assert!(
5617 Options::try_parse_from([
5618 "openvmm",
5619 "--igvm",
5620 "guest.igvm",
5621 "--igvm-personality",
5622 "linux-direct",
5623 firmware,
5624 ])
5625 .is_err()
5626 );
5627 }
5628 }
5629
5630 #[test]
5631 fn test_pidfile_option_parsed() {
5632 let opt = Options::try_parse_from(["openvmm", "--pidfile", "/tmp/test.pid"]).unwrap();
5633 assert_eq!(opt.pidfile, Some(PathBuf::from("/tmp/test.pid")));
5634 }
5635
5636 #[test]
5637 fn test_tpm_version_option() {
5638 let opt = Options::try_parse_from(["openvmm"]).unwrap();
5639 assert_eq!(opt.tpm, None);
5640
5641 let opt = Options::try_parse_from(["openvmm", "--tpm"]).unwrap();
5642 assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
5643
5644 let opt = Options::try_parse_from(["openvmm", "--tpm", "--uefi"]).unwrap();
5645 assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
5646 assert!(opt.uefi);
5647
5648 let opt = Options::try_parse_from(["openvmm", "--tpm", "138"]).unwrap();
5649 assert_eq!(opt.tpm, Some(TpmVersionCli::V138));
5650
5651 let opt = Options::try_parse_from(["openvmm", "--tpm=185"]).unwrap();
5652 assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
5653
5654 let opt = Options::try_parse_from(["openvmm", "--tpm", "1.38"]).unwrap();
5655 assert_eq!(opt.tpm, Some(TpmVersionCli::V138));
5656
5657 assert!(Options::try_parse_from(["openvmm", "--tpm", "137"]).is_err());
5658 }
5659
5660 #[test]
5661 fn test_guest_power_action_flags() {
5662 let opt = Options::try_parse_from(["openvmm"]).unwrap();
5665 assert_eq!(opt.guest_reset_action, GuestPowerAction::Reset);
5666 assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Halt);
5667 assert_eq!(opt.guest_crash_action, GuestPowerAction::Halt);
5668 assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Reset);
5669 assert_eq!(
5672 crate::vm_controller::GuestPowerActions {
5673 shutdown: opt.guest_shutdown_action,
5674 reset: opt.guest_reset_action,
5675 crash: opt.guest_crash_action,
5676 watchdog: opt.guest_watchdog_action,
5677 },
5678 crate::vm_controller::GuestPowerActions::default(),
5679 );
5680
5681 let opt = Options::try_parse_from([
5682 "openvmm",
5683 "--guest-watchdog",
5684 "--guest-reset-action",
5685 "exit",
5686 "--guest-shutdown-action",
5687 "exit:5",
5688 "--guest-crash-action",
5689 "reset",
5690 "--guest-watchdog-action",
5691 "halt",
5692 ])
5693 .unwrap();
5694 assert_eq!(opt.guest_reset_action, GuestPowerAction::Exit(0));
5696 assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Exit(5));
5697 assert_eq!(opt.guest_crash_action, GuestPowerAction::Reset);
5698 assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Halt);
5699
5700 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:nope"]).is_err());
5702 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:300"]).is_err());
5703 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:-1"]).is_err());
5704
5705 assert!(Options::try_parse_from(["openvmm", "--guest-watchdog-action", "halt"]).is_err());
5707 }
5708
5709 #[cfg(target_os = "linux")]
5710 #[test]
5711 fn test_vfio_device_cli_parse() {
5712 use vfio_assigned_device_resources::BarAddressConfig;
5713
5714 let v = VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0").unwrap();
5716 assert_eq!(v.pci_id, "0000:01:00.0");
5717 assert_eq!(v.port_name, "rp0");
5718 assert_eq!(v.iommu, None);
5719
5720 let v = VfioDeviceCli::from_str("port=rp1,iommu=iommu0,host=0000:02:00.0").unwrap();
5722 assert_eq!(v.pci_id, "0000:02:00.0");
5723 assert_eq!(v.port_name, "rp1");
5724 assert_eq!(v.iommu.as_deref(), Some("iommu0"));
5725
5726 let v = VfioDeviceCli::from_str(
5727 "host=0000:03:00.0,port=rp2,bar0=host,bar2=0x80000000,bar4=0x110000000000",
5728 )
5729 .unwrap();
5730 assert_eq!(v.bar_addresses[0], BarAddressConfig::HostAssigned);
5731 assert_eq!(v.bar_addresses[1], BarAddressConfig::GuestAssigned);
5732 assert_eq!(v.bar_addresses[2], BarAddressConfig::Fixed(0x80000000));
5733 assert_eq!(v.bar_addresses[4], BarAddressConfig::Fixed(0x110000000000));
5734 }
5735
5736 #[cfg(target_os = "linux")]
5737 #[test]
5738 fn test_vfio_device_cli_errors() {
5739 assert!(VfioDeviceCli::from_str("port=rp0").is_err());
5741 assert!(VfioDeviceCli::from_str("host=0000:01:00.0").is_err());
5742
5743 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,foo=bar").is_err());
5745
5746 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,host=0000:02:00.0,port=rp0").is_err());
5748 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,port=rp1").is_err());
5749 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=a,iommu=b").is_err());
5750
5751 assert!(VfioDeviceCli::from_str("host=,port=rp0").is_err());
5753 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=").is_err());
5754 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=").is_err());
5755
5756 assert!(VfioDeviceCli::from_str("host").is_err());
5758 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu").is_err());
5759
5760 assert!(VfioDeviceCli::from_str("host=../../etc/passwd,port=rp0").is_err());
5762 assert!(VfioDeviceCli::from_str("host=foo/bar,port=rp0").is_err());
5763
5764 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0").is_err());
5766 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x0").is_err());
5767 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0xnope").is_err());
5768 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=pt").is_err());
5769 assert!(
5770 VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x1000,bar0=host").is_err()
5771 );
5772 }
5773
5774 #[cfg(target_os = "linux")]
5775 #[test]
5776 fn test_iommu_cli_parse() {
5777 let c = IommuCli::from_str("id=iommu0").unwrap();
5778 assert_eq!(c.id, "iommu0");
5779
5780 assert!(IommuCli::from_str("name=iommu0").is_err());
5782
5783 assert!(IommuCli::from_str("iommu0").is_err());
5785
5786 assert!(IommuCli::from_str("id=").is_err());
5788 }
5789
5790 #[cfg(target_os = "linux")]
5791 #[test]
5792 fn test_vhost_user_cli() {
5793 let v = VhostUserCli::from_str("/run/blk.sock,type=blk,num_queues=2").unwrap();
5795 assert_eq!(v.socket_path, "/run/blk.sock");
5796 assert!(matches!(
5797 v.device_type,
5798 VhostUserDeviceTypeCli::Blk {
5799 num_queues: Some(2),
5800 queue_size: None
5801 }
5802 ));
5803 assert_eq!(v.pcie_port, None);
5804
5805 let v = VhostUserCli::from_str("/run/fs.sock,type=fs,tag=myfs,pcie_port=p0").unwrap();
5807 assert!(matches!(
5808 &v.device_type,
5809 VhostUserDeviceTypeCli::Fs { tag, .. } if tag == "myfs"
5810 ));
5811 assert_eq!(v.pcie_port.as_deref(), Some("p0"));
5812
5813 let v = VhostUserCli::from_str("/run/x.sock,device_id=9,queue_sizes=[16,32]").unwrap();
5815 assert!(matches!(
5816 &v.device_type,
5817 VhostUserDeviceTypeCli::Other { device_id: 9, queue_sizes } if *queue_sizes == vec![16, 32]
5818 ));
5819
5820 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()); }
5831
5832 #[cfg(target_os = "linux")]
5833 #[test]
5834 fn test_vhost_vsock_cli() {
5835 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "3"]).unwrap();
5836 assert_eq!(opt.virtio_vsock_vhost_cid, Some(3));
5837
5838 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "2"]).is_err());
5839 assert!(
5840 Options::try_parse_from([
5841 "openvmm",
5842 "--virtio-vsock-vhost-cid",
5843 "3",
5844 "--virtio-vsock-path",
5845 "/tmp/vsock",
5846 ])
5847 .is_err()
5848 );
5849 }
5850
5851 #[test]
5852 fn test_virtio_vsock_bus_cli() {
5853 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "mmio"]).unwrap();
5854 assert!(matches!(opt.virtio_vsock_bus, Some(VirtioBusCli::Mmio)));
5855
5856 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pci"]).unwrap();
5857 assert!(matches!(opt.virtio_vsock_bus, Some(VirtioBusCli::Pci)));
5858
5859 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "auto"]).is_err());
5860 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "vpci"]).is_err());
5861 }
5862
5863 #[test]
5864 fn test_nvme_controller_cli_pcie() {
5865 let c = NvmeControllerCli::from_str("id=nvme0,pcie_port=p0").unwrap();
5866 assert_eq!(c.id, "nvme0");
5867 assert_eq!(c.transport, NvmeControllerTransport::Pcie("p0".into()));
5868 }
5869
5870 #[test]
5871 fn test_nvme_controller_cli_vpci_no_guid() {
5872 let c = NvmeControllerCli::from_str("id=nvme1,vpci").unwrap();
5873 assert_eq!(c.id, "nvme1");
5874 assert!(matches!(c.transport, NvmeControllerTransport::Vpci(None)));
5875 assert!(NvmeControllerCli::from_str("id=nvme1,vpci=").is_err());
5876 }
5877
5878 #[test]
5879 fn test_nvme_controller_cli_vpci_with_guid() {
5880 let c = NvmeControllerCli::from_str("id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c")
5881 .unwrap();
5882 assert_eq!(c.id, "nvme2");
5883 assert!(matches!(
5884 c.transport,
5885 NvmeControllerTransport::Vpci(Some(_))
5886 ));
5887 }
5888
5889 #[test]
5890 fn test_nvme_controller_cli_errors() {
5891 assert!(NvmeControllerCli::from_str("pcie_port=p0").is_err());
5893 assert!(NvmeControllerCli::from_str("id=nvme0").is_err());
5895 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,vpci").is_err());
5897 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,foo=bar").is_err());
5899 assert!(NvmeControllerCli::from_str("id=,pcie_port=p0").is_err());
5901 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=").is_err());
5903 let err = NvmeControllerCli::from_str("id=nvme0,vpci=not-a-guid").unwrap_err();
5905 assert!(err.to_string().contains("invalid value for option 'vpci'"));
5906 }
5907
5908 #[test]
5909 fn test_disk_cli_controller() {
5910 let d = DiskCli::from_str("file:disk.vhd,on=nvme0").unwrap();
5911 assert_eq!(d.controller.as_deref(), Some("nvme0"));
5912 assert_eq!(d.nsid, None);
5913 }
5914
5915 #[test]
5916 fn test_disk_cli_controller_with_nsid() {
5917 let d = DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=3").unwrap();
5918 assert_eq!(d.controller.as_deref(), Some("nvme0"));
5919 assert_eq!(d.nsid, Some(3));
5920 }
5921
5922 #[test]
5923 fn test_disk_cli_controller_errors() {
5924 assert!(DiskCli::from_str("file:disk.vhd,nsid=1").is_err());
5926 assert!(DiskCli::from_str("file:disk.vhd,lun=0").is_err());
5928 assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,pcie_port=p0").is_err());
5930 assert!(DiskCli::from_str("file:disk.vhd,on=").is_err());
5932 assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=abc").is_err());
5934 assert!(DiskCli::from_str("file:disk.vhd,on=c,nsid=1,lun=0").is_err());
5936 }
5937
5938 #[test]
5939 fn test_disk_cli_controller_with_lun() {
5940 let d = DiskCli::from_str("file:disk.vhd,on=scsi0,lun=3").unwrap();
5941 assert_eq!(d.controller.as_deref(), Some("scsi0"));
5942 assert_eq!(d.lun, Some(3));
5943 assert_eq!(d.nsid, None);
5944 }
5945
5946 #[test]
5947 fn test_scsi_controller_cli() {
5948 let c = ScsiControllerCli::from_str("id=scsi0").unwrap();
5949 assert_eq!(c.id, "scsi0");
5950 assert_eq!(c.sub_channels, 0);
5951 }
5952
5953 #[test]
5954 fn test_scsi_controller_cli_with_sub_channels() {
5955 let c = ScsiControllerCli::from_str("id=scsi1,sub_channels=4").unwrap();
5956 assert_eq!(c.id, "scsi1");
5957 assert_eq!(c.sub_channels, 4);
5958 }
5959
5960 #[test]
5961 fn test_scsi_controller_cli_errors() {
5962 assert!(ScsiControllerCli::from_str("sub_channels=4").is_err());
5964 assert!(ScsiControllerCli::from_str("id=").is_err());
5966 assert!(ScsiControllerCli::from_str("id=scsi0,foo=bar").is_err());
5968 assert!(ScsiControllerCli::from_str("id=scsi0,sub_channels=abc").is_err());
5970 }
5971
5972 #[test]
5973 fn test_disk_cli_relay() {
5974 let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt").unwrap();
5975 assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
5976 assert_eq!(d.relay.as_ref().unwrap().1, None);
5977 }
5978
5979 #[test]
5980 fn test_disk_cli_relay_with_location() {
5981 let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:3").unwrap();
5982 assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
5983 assert_eq!(d.relay.as_ref().unwrap().1, Some(3));
5984 }
5985
5986 #[test]
5987 fn test_disk_cli_relay_errors() {
5988 assert!(DiskCli::from_str("file:disk.vhd,relay=tgt").is_err());
5990 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt,uh").is_err());
5992 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:abc").is_err());
5994 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=").is_err());
5996 }
5997
5998 #[test]
5999 fn test_nvme_controller_cli_vtl2() {
6000 let c = NvmeControllerCli::from_str("id=nvme0,vpci,vtl2").unwrap();
6001 assert_eq!(c.vtl, DeviceVtl::Vtl2);
6002 }
6003
6004 #[test]
6005 fn test_scsi_controller_cli_vtl2() {
6006 let c = ScsiControllerCli::from_str("id=scsi0,vtl2").unwrap();
6007 assert_eq!(c.vtl, DeviceVtl::Vtl2);
6008 }
6009
6010 #[test]
6011 fn test_openhcl_controller_cli() {
6012 let c = OpenhclControllerCli::from_str("id=vtl0-scsi,type=scsi").unwrap();
6013 assert_eq!(c.id, "vtl0-scsi");
6014 assert_eq!(c.controller_type, OpenhclControllerType::Scsi);
6015 assert_eq!(c.guid, None);
6016 }
6017
6018 #[test]
6019 fn test_openhcl_controller_cli_nvme_with_guid() {
6020 let c = OpenhclControllerCli::from_str(
6021 "id=vtl0-nvme,type=nvme,guid=09a59b81-2bf6-4164-81d7-3a0dc977ba65",
6022 )
6023 .unwrap();
6024 assert_eq!(c.controller_type, OpenhclControllerType::Nvme);
6025 assert!(c.guid.is_some());
6026 }
6027
6028 #[test]
6029 fn test_openhcl_controller_cli_errors() {
6030 assert!(OpenhclControllerCli::from_str("type=scsi").is_err());
6032 assert!(OpenhclControllerCli::from_str("id=foo").is_err());
6034 assert!(OpenhclControllerCli::from_str("id=foo,type=ide").is_err());
6036 assert!(OpenhclControllerCli::from_str("id=foo,type=scsi,guid=bad").is_err());
6038 }
6039
6040 #[test]
6041 fn test_parse_vp_list() {
6042 use vmm_cli::BracketRangeList;
6043
6044 let parse = |s: &str| {
6045 s.parse::<BracketRangeList>()
6046 .and_then(|v| v.expand_below(1024))
6047 };
6048
6049 assert_eq!(parse("[0,1,2,3]").unwrap(), vec![0, 1, 2, 3]);
6051
6052 assert_eq!(parse("[5]").unwrap(), vec![5]);
6054
6055 assert_eq!(parse("[0-3]").unwrap(), vec![0, 1, 2, 3]);
6057
6058 assert_eq!(parse("[0,1,4-6,10]").unwrap(), vec![0, 1, 4, 5, 6, 10]);
6060
6061 assert_eq!(parse("[0, 1, 2-4]").unwrap(), vec![0, 1, 2, 3, 4]);
6063
6064 assert!(parse("0,1,2").is_err());
6066 assert!(parse("0-3").is_err());
6067
6068 assert!(parse("[3-0]").is_err());
6070
6071 assert!(parse("[a,b]").is_err());
6073 }
6074
6075 #[test]
6076 fn test_parse_numa_node() {
6077 use super::parse_numa_node;
6078
6079 let n = parse_numa_node("size=2G").unwrap();
6081 assert_eq!(
6082 n.memory.size,
6083 Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024))
6084 );
6085 assert!(n.vps.is_none());
6086 assert!(n.host_numa_node.is_none());
6087
6088 let n = parse_numa_node("size=1G,vps=[0,1,2,3]").unwrap();
6090 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
6091
6092 let n = parse_numa_node("size=1G,vps=[0-3]").unwrap();
6094 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
6095
6096 let n = parse_numa_node("size=1G,host_numa_node=1").unwrap();
6098 assert_eq!(n.host_numa_node, Some(1));
6099
6100 let n = parse_numa_node("size=1G,vps=[0,1],host_numa_node=0,hugepages=on").unwrap();
6102 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1]);
6103 assert_eq!(n.host_numa_node, Some(0));
6104 assert!(n.memory.hugepages);
6105
6106 assert!(parse_numa_node("vps=[0,1]").is_err());
6108
6109 assert!(parse_numa_node("size=1G,vps=0,1").is_err());
6111
6112 assert!(parse_numa_node("size=1G,vps=[0],vps=[1]").is_err());
6114
6115 assert!(parse_numa_node("size=1G,file=/tmp/x").is_err());
6117
6118 let n = parse_numa_node("size=1G,vps=[]").unwrap();
6120 assert!(n.vps.unwrap().0.is_empty());
6121 }
6122
6123 #[test]
6124 fn test_parse_numa_distance() {
6125 use super::parse_numa_distance;
6126
6127 let d = parse_numa_distance("0:1:20").unwrap();
6128 assert_eq!(d.src, 0);
6129 assert_eq!(d.dst, 1);
6130 assert_eq!(d.distance, 20);
6131
6132 let d = parse_numa_distance("0:0:10").unwrap();
6134 assert_eq!(d.distance, 10);
6135
6136 assert!(parse_numa_distance("0:1:5").is_err());
6138
6139 assert!(parse_numa_distance("0:1").is_err());
6141 assert!(parse_numa_distance("0:1:20:extra").is_err());
6142 }
6143
6144 #[cfg(guest_arch = "aarch64")]
6145 #[test]
6146 fn test_smmu_cli_from_str() {
6147 let s = SmmuCli::from_str("rc=pcie0").unwrap();
6149 assert_eq!(s.rc_name, "pcie0");
6150 assert!(!s.accel);
6151 assert!(matches!(s.oas, SmmuOasCli::Auto));
6152
6153 let s = SmmuCli::from_str("rc=pcie0,accel").unwrap();
6155 assert_eq!(s.rc_name, "pcie0");
6156 assert!(s.accel);
6157 assert!(matches!(s.oas, SmmuOasCli::Auto));
6158
6159 let s = SmmuCli::from_str("rc=pcie0,oas=auto").unwrap();
6161 assert!(matches!(s.oas, SmmuOasCli::Auto));
6162
6163 let s = SmmuCli::from_str("rc=pcie0,oas=52").unwrap();
6165 assert!(matches!(s.oas, SmmuOasCli::Fixed(52)));
6166
6167 let s = SmmuCli::from_str("oas=48,accel,rc=pcie1").unwrap();
6169 assert_eq!(s.rc_name, "pcie1");
6170 assert!(s.accel);
6171 assert!(matches!(s.oas, SmmuOasCli::Fixed(48)));
6172
6173 assert!(SmmuCli::from_str("accel").is_err());
6175 assert!(SmmuCli::from_str("oas=52").is_err());
6176
6177 assert!(SmmuCli::from_str("rc=").is_err());
6179
6180 assert!(SmmuCli::from_str("rc=pcie0,rc=pcie1").is_err());
6182
6183 assert!(SmmuCli::from_str("rc=pcie0,oas=big").is_err());
6185
6186 assert!(SmmuCli::from_str("rc=pcie0,foo=bar").is_err());
6188
6189 assert!(SmmuCli::from_str("rc=pcie0,turbo").is_err());
6191 }
6192}