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)]
146pub struct Options {
147 #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
149 pub processors: u32,
150
151 #[clap(
153 short = 'm',
154 long,
155 value_name = "PARAMS",
156 default_value = "1GB",
157 value_parser = parse_memory_config,
158 conflicts_with = "numa",
159 long_help = r#"Configure guest RAM.
160
161Syntax: SIZE | key=value[,key=value...]
162
163Size suffixes accept K, M, G, and T, optionally followed by B.
164
165Options:
166 size=<SIZE> guest RAM size, default 1GB
167 shared[=on|off] use shared file-backed RAM, default on
168 prefetch[=on|off] pre-populate guest RAM mappings
169 thp[=on|off] mark guest RAM as THP-eligible (Linux), default on
170 hugepages[=on|off] allocate RAM from hugetlb/large pages (Linux, Windows)
171 hugepage_size=<SIZE> hugepage size, default 2MB; requires hugepages=on
172 file=<PATH> use an existing file as guest RAM backing
173
174Examples:
175 --memory 4G
176 --memory size=64GB,hugepages=on,hugepage_size=2MB
177 --memory size=4G,file=path/to/memory.bin
178 --memory size=4G,thp=off"#
179 )]
180 pub memory: MemoryCli,
181
182 #[clap(
187 long,
188 value_name = "PARAMS",
189 value_parser = parse_numa_node,
190 conflicts_with = "memory",
191 long_help = r#"Configure a guest NUMA node (repeatable, one per node).
192
193Syntax: key=value[,key=value...]
194
195Options:
196 size=<SIZE> RAM for this node (required)
197 shared[=on|off] use shared file-backed RAM, default on
198 prefetch[=on|off] pre-populate guest RAM mappings
199 thp[=on|off] mark node RAM as THP-eligible (Linux), default on
200 hugepages[=on|off] allocate RAM from hugetlb/large pages (Linux, Windows)
201 hugepage_size=<SIZE> hugepage size, default 2MB; requires hugepages=on
202 host_numa_node=<N> bind allocation to host NUMA node N
203 vps=<LIST> explicit VP indices (e.g. "[0,1,2,3]")
204
205 VP lists use bracket syntax with comma-separated indices and dash
206 ranges: vps=[0,1] or vps=[0-3] or vps=[0,1,4-5]. An empty list, vps=[],
207 declares a CPU-less node (e.g. a generic-initiator target); unlike a
208 non-empty list, it may be combined with nodes that omit vps.
209
210Examples:
211 --numa size=2G --numa size=2G
212 --numa size=2G,host_numa_node=0 --numa size=2G,host_numa_node=1
213 --numa size=2G,hugepages=on,vps=[0,1] --numa size=2G,vps=[2,3]
214 --numa size=2G,vps=[0-3] --numa size=2G,vps=[4-7]
215 --numa size=2G --numa size=0,vps=[]"#
216 )]
217 pub numa: Option<Vec<NumaNodeCli>>,
218
219 #[clap(long, value_name = "SRC:DST:DIST", value_parser = parse_numa_distance, conflicts_with = "memory", requires = "numa")]
224 pub numa_distance: Option<Vec<NumaDistanceCli>>,
225
226 #[clap(short = 'M', long, hide = true)]
228 pub shared_memory: bool,
229
230 #[clap(long = "prefetch", hide = true, conflicts_with = "numa")]
232 pub deprecated_prefetch: bool,
233
234 #[clap(
238 long = "memory-backing-file",
239 value_name = "FILE",
240 hide = true,
241 conflicts_with_all = ["deprecated_private_memory", "numa"]
242 )]
243 pub deprecated_memory_backing_file: Option<PathBuf>,
244
245 #[clap(
248 long,
249 value_name = "DIR",
250 conflicts_with_all = ["deprecated_memory_backing_file", "numa"]
251 )]
252 pub restore_snapshot: Option<PathBuf>,
253
254 #[clap(long = "private-memory", hide = true, conflicts_with_all = ["deprecated_memory_backing_file", "restore_snapshot", "numa"])]
256 pub deprecated_private_memory: bool,
257
258 #[clap(long = "thp", hide = true, conflicts_with = "numa")]
260 pub deprecated_thp: bool,
261
262 #[clap(short = 'P', long)]
264 pub paused: bool,
265
266 #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
268 pub kernel: OptionalPathBuf,
269
270 #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
272 pub initrd: OptionalPathBuf,
273
274 #[clap(short = 'c', long, value_name = "STRING")]
276 pub cmdline: Vec<String>,
277
278 #[clap(long)]
280 pub hv: bool,
281
282 #[clap(long, conflicts_with_all = ["uefi", "pcat", "igvm"])]
286 pub device_tree: bool,
287
288 #[clap(long, requires("hv"))]
292 pub vtl2: bool,
293
294 #[clap(long, requires("hv"))]
297 pub get: bool,
298
299 #[clap(long, conflicts_with("get"))]
302 pub no_get: bool,
303
304 #[clap(
306 long,
307 conflicts_with_all = [
308 "vmbus_vsock_path",
309 "vmbus_vtl2_vsock_path",
310 "vmbus_redirect",
311 "vmbus_max_version",
312 "vmbus_com1_serial",
313 "vmbus_com2_serial",
314 "vtl2",
315 "get",
316 "pcat",
317 ],
318 )]
319 pub no_vmbus: bool,
320
321 #[clap(long, requires("vtl2"))]
323 pub no_alias_map: bool,
324
325 #[clap(long, requires("vtl2"))]
327 pub isolation: Option<IsolationCli>,
328
329 #[clap(long, value_name = "PATH", alias = "vsock-path")]
331 pub vmbus_vsock_path: Option<String>,
332
333 #[clap(long, value_name = "PATH", requires("vtl2"), alias = "vtl2-vsock-path")]
335 pub vmbus_vtl2_vsock_path: Option<String>,
336
337 #[clap(long, requires("vtl2"), default_value = "halt")]
339 pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
340
341 #[clap(long_help = r#"
343e.g: --disk memdiff:file:/path/to/disk.vhd
344
345syntax: <path> | kind:<arg>[,flag,opt=arg,...]
346
347valid disk kinds:
348 `mem:<len>` memory backed disk
349 <len>: length of ramdisk, e.g.: `1G`
350 `memdiff:<disk>` memory backed diff disk
351 <disk>: lower disk, e.g.: `file:base.img`
352 `file:<path>[;direct][;create=<len>]` file-backed disk
353 <path>: path to file
354 `;direct`: bypass the OS page cache
355 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
356 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
357 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
358 `blob:<type>:<url>` HTTP blob (read-only)
359 <type>: `flat` or `vhd1`
360 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
361 <cipher>: `xts-aes-256`
362 `prwrap:<disk>` persistent reservations wrapper
363
364flags:
365 `ro` open disk as read-only
366 `dvd` specifies that device is cd/dvd and it is read_only
367 `vtl2` assign this disk to VTL2
368 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
369 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
370
371options:
372 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
373 `on=<name>` attach to a named controller (NVMe or SCSI), incompatible with `pcie_port` and `vtl2`
374 `nsid=<N>` NVMe namespace ID (1-based), requires `on`; auto-assigned if omitted
375 `lun=<N>` SCSI LUN (0-based), requires `on`; auto-assigned if omitted
376 `relay=<ctrl>[:<loc>]` relay through OpenHCL to the named OpenHCL controller, with optional location (LUN or NSID)
377"#)]
378 #[clap(long, value_name = "FILE")]
379 pub disk: Vec<DiskCli>,
380
381 #[clap(long_help = r#"
385e.g: --nvme memdiff:file:/path/to/disk.vhd
386
387syntax: <path> | kind:<arg>[,flag,opt=arg,...]
388
389valid disk kinds:
390 `mem:<len>` memory backed disk
391 <len>: length of ramdisk, e.g.: `1G`
392 `memdiff:<disk>` memory backed diff disk
393 <disk>: lower disk, e.g.: `file:base.img`
394 `file:<path>[;direct][;create=<len>]` file-backed disk
395 <path>: path to file
396 `;direct`: bypass the OS page cache
397 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
398 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
399 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
400 `blob:<type>:<url>` HTTP blob (read-only)
401 <type>: `flat` or `vhd1`
402 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
403 <cipher>: `xts-aes-256`
404 `prwrap:<disk>` persistent reservations wrapper
405
406flags:
407 `ro` open disk as read-only
408 `vtl2` assign this disk to VTL2
409 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
410 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
411
412options:
413 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
414"#)]
415 #[clap(long)]
416 pub nvme: Vec<DiskCli>,
417
418 #[clap(long_help = r#"
420Create a named NVMe controller with an explicit transport.
421
422syntax: id=<name>,pcie_port=<port> | id=<name>,vpci[=<guid>]
423
424The controller name can be referenced by `--disk` with the `on=<name>`
425option to attach namespaces to this controller.
426
427options:
428 `id=<name>` controller name (required)
429 `pcie_port=<port>` present on PCIe under the specified port
430 `vpci[=<guid>]` present via VPCI; optional instance GUID
431 `vtl2` assign to VTL2 (default VTL0)
432
433Exactly one of `pcie_port` or `vpci` must be specified.
434
435Examples:
436 --nvme-pci id=nvme0,pcie_port=p0
437 --nvme-pci id=nvme1,vpci
438 --nvme-pci id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c
439"#)]
440 #[clap(long = "nvme-pci")]
441 pub nvme_pci: Vec<NvmeControllerCli>,
442
443 #[clap(long_help = r#"
445Create a named VMBus SCSI controller.
446
447syntax: id=<name>[,sub_channels=<N>][,vtl2]
448
449The controller name can be referenced by `--disk` with the `on=<name>`
450option to attach disks to this controller.
451
452options:
453 `id=<name>` controller name (required)
454 `sub_channels=<N>` number of sub-channels (default 0)
455 `vtl2` assign to VTL2 (default VTL0)
456
457Examples:
458 --vmbus-scsi id=scsi0
459 --vmbus-scsi id=scsi1,sub_channels=4
460"#)]
461 #[clap(long = "vmbus-scsi")]
462 pub vmbus_scsi: Vec<ScsiControllerCli>,
463
464 #[clap(long_help = r#"
466Register an OpenHCL-managed storage controller that can be used as a
467relay target with `--disk ... relay=<name>`.
468
469syntax: id=<name>,type=scsi|nvme[,guid=<guid>]
470
471options:
472 `id=<name>` controller name (required)
473 `type=scsi|nvme` controller protocol (required)
474 `guid=<guid>` instance GUID (auto-derived from name if omitted)
475
476Examples:
477 --openhcl-controller id=vtl0-scsi,type=scsi
478 --openhcl-controller id=vtl0-nvme,type=nvme,guid=09a59b81-...
479"#)]
480 #[clap(long = "openhcl-controller")]
481 pub openhcl_controller: Vec<OpenhclControllerCli>,
482
483 #[clap(long = "cxl-test", value_name = "mem:<len>,pcie_port=<name>")]
485 pub cxl_test: Vec<CxlTestDeviceCli>,
486
487 #[clap(long_help = r#"
489e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
490
491syntax: <path> | kind:<arg>[,flag,opt=arg,...]
492
493valid disk kinds:
494 `mem:<len>` memory backed disk
495 <len>: length of ramdisk, e.g.: `1G`
496 `memdiff:<disk>` memory backed diff disk
497 <disk>: lower disk, e.g.: `file:base.img`
498 `file:<path>[;direct]` file-backed disk
499 <path>: path to file
500 `;direct`: bypass the OS page cache
501
502flags:
503 `ro` open disk as read-only
504
505options:
506 `pcie_port=<name>` present the disk using pcie under the specified port
507"#)]
508 #[clap(long = "virtio-blk")]
509 pub virtio_blk: Vec<DiskCli>,
510
511 #[cfg(target_os = "linux")]
536 #[clap(long = "vhost-user")]
537 pub vhost_user: Vec<VhostUserCli>,
538
539 #[clap(long, value_name = "COUNT", default_value = "0")]
541 pub scsi_sub_channels: u16,
542
543 #[clap(long)]
545 pub nic: bool,
546
547 #[clap(long)]
559 pub net: Vec<NicConfigCli>,
560
561 #[clap(long, value_name = "SWITCH_ID")]
565 pub kernel_vmnic: Vec<String>,
566
567 #[clap(long)]
569 pub gfx: bool,
570
571 #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
573 pub vtl2_gfx: bool,
574
575 #[clap(flatten)]
577 pub vnc: VncCli,
578
579 #[cfg(guest_arch = "x86_64")]
581 #[clap(long, default_value_t)]
582 pub apic_id_offset: u32,
583
584 #[clap(long)]
586 pub vps_per_socket: Option<u32>,
587
588 #[clap(long, default_value = "auto")]
590 pub smt: SmtConfigCli,
591
592 #[cfg(guest_arch = "x86_64")]
594 #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
595 pub x2apic: X2ApicConfig,
596
597 #[cfg(guest_arch = "aarch64")]
599 #[clap(long, default_value = "auto")]
600 pub gic_msi: GicMsiCli,
601
602 #[cfg(guest_arch = "aarch64")]
606 #[clap(long, value_name = "SMMU_CONFIG")]
607 pub smmu: Vec<SmmuCli>,
608
609 #[clap(long, value_name = "SERIAL")]
621 pub com1: Option<ComSerialConfigCli>,
622
623 #[clap(long, value_name = "SERIAL")]
626 pub com2: Option<ComSerialConfigCli>,
627
628 #[clap(long, value_name = "SERIAL")]
631 pub com3: Option<ComSerialConfigCli>,
632
633 #[clap(long, value_name = "SERIAL")]
636 pub com4: Option<ComSerialConfigCli>,
637
638 #[structopt(long, value_name = "SERIAL")]
640 pub vmbus_com1_serial: Option<SerialConfigCli>,
641
642 #[structopt(long, value_name = "SERIAL")]
644 pub vmbus_com2_serial: Option<SerialConfigCli>,
645
646 #[clap(long)]
648 pub serial_tx_only: bool,
649
650 #[clap(long, value_name = "SERIAL")]
652 pub debugcon: Option<DebugconSerialConfigCli>,
653
654 #[clap(long, short = 'e')]
656 pub uefi: bool,
657
658 #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
660 pub uefi_firmware: OptionalPathBuf,
661
662 #[clap(long, requires("uefi"))]
664 pub uefi_debug: bool,
665
666 #[clap(long, requires("uefi"))]
668 pub uefi_enable_memory_protections: bool,
669
670 #[clap(long, requires("uefi"))]
672 pub uefi_force_dma_bounce: bool,
673
674 #[clap(long, requires("pcat"))]
685 pub pcat_boot_order: Option<PcatBootOrderCli>,
686
687 #[clap(long, conflicts_with("uefi"))]
689 pub pcat: bool,
690
691 #[clap(long, requires("pcat"), value_name = "FILE")]
693 pub pcat_firmware: Option<PathBuf>,
694
695 #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
697 pub igvm: Option<PathBuf>,
698
699 #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
702 pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
703
704 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
709 pub virtio_9p: Vec<FsArgs>,
710
711 #[clap(long)]
713 pub virtio_9p_debug: bool,
714
715 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
720 pub virtio_fs: Vec<FsArgsWithOptions>,
721
722 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
727 pub virtio_fs_shmem: Vec<FsArgs>,
728
729 #[clap(long, value_name = "BUS", default_value = "auto")]
731 pub virtio_fs_bus: VirtioBusCli,
732
733 #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
738 pub virtio_pmem: Option<VirtioPmemArgs>,
739
740 #[clap(long)]
742 pub virtio_rng: bool,
743
744 #[clap(long, value_name = "BUS", default_value = "auto")]
746 pub virtio_rng_bus: VirtioBusCli,
747
748 #[clap(long, value_name = "PORT", requires("virtio_rng"))]
750 pub virtio_rng_pcie_port: Option<String>,
751
752 #[clap(long)]
758 pub virtio_console: Option<SerialConfigCli>,
759
760 #[clap(long, value_name = "PORT", requires("virtio_console"))]
762 pub virtio_console_pcie_port: Option<String>,
763
764 #[clap(long, value_name = "PATH")]
766 pub virtio_vsock_path: Option<String>,
767
768 #[cfg(target_os = "linux")]
771 #[clap(
772 long,
773 value_name = "CID",
774 conflicts_with = "virtio_vsock_path",
775 value_parser = parse_vhost_vsock_cid
776 )]
777 pub virtio_vsock_vhost_cid: Option<u32>,
778
779 #[clap(long)]
786 pub virtio_net: Vec<NicConfigCli>,
787
788 #[clap(long, value_name = "PATH")]
790 pub log_file: Option<PathBuf>,
791
792 #[clap(long, value_name = "PATH")]
796 pub pidfile: Option<PathBuf>,
797
798 #[clap(long, value_name = "SOCKETPATH")]
802 pub ttrpc: Option<PathBuf>,
803
804 #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
808 pub grpc: Option<PathBuf>,
809
810 #[clap(long_help = r#"
812Run as an RPC server on the specified Unix socket.
813
814syntax: path=<PATH>[,transport=<TRANSPORT>]
815
816options:
817 `path=<PATH>` Unix socket path to listen on (required)
818 `transport=<TRANSPORT>` wire transport to accept (default: auto)
819
820valid transports:
821 `auto` auto-detect ttrpc vs. gRPC per connection
822 `ttrpc` accept ttrpc clients only
823 `grpc` accept gRPC clients only
824
825Examples:
826 --rpc path=/tmp/openvmm.sock
827 --rpc path=/tmp/openvmm.sock,transport=ttrpc
828"#)]
829 #[clap(
830 long,
831 value_name = "path=PATH[,transport=TRANSPORT]",
832 conflicts_with("ttrpc"),
833 conflicts_with("grpc")
834 )]
835 pub rpc: Option<RpcCli>,
836
837 #[clap(long)]
839 pub single_process: bool,
840
841 #[cfg(windows)]
843 #[clap(long, value_name = "PATH")]
844 pub device: Vec<String>,
845
846 #[clap(long, requires("uefi"))]
848 pub disable_frontpage: bool,
849
850 #[clap(long)]
852 pub tpm: bool,
853
854 #[clap(long, default_value = "control", hide(true))]
858 #[expect(clippy::option_option)]
859 pub internal_worker: Option<Option<String>>,
860
861 #[clap(long, requires("vtl2"))]
863 pub vmbus_redirect: bool,
864
865 #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
867 pub vmbus_max_version: Option<u32>,
868
869 #[clap(long_help = r#"
873e.g: --vmgs memdiff:file:/path/to/file.vmgs
874
875syntax: <path> | kind:<arg>[,flag]
876
877valid disk kinds:
878 `mem:<len>` memory backed disk
879 <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
880 `memdiff:<disk>[;create=<len>]` memory backed diff disk
881 <disk>: lower disk, e.g.: `file:base.img`
882 `file:<path>` file-backed disk
883 <path>: path to file
884
885flags:
886 `fmt` reprovision the VMGS before boot
887 `fmt-on-fail` reprovision the VMGS before boot if it is corrupted
888"#)]
889 #[clap(long)]
890 pub vmgs: Option<VmgsCli>,
891
892 #[clap(long, requires("vmgs"))]
894 pub test_gsp_by_id: bool,
895
896 #[clap(long, requires("pcat"), value_name = "FILE")]
898 pub vga_firmware: Option<PathBuf>,
899
900 #[clap(long)]
902 pub secure_boot: bool,
903
904 #[clap(long)]
906 pub secure_boot_template: Option<SecureBootTemplateCli>,
907
908 #[clap(long, value_name = "PATH")]
910 pub custom_uefi_json: Option<PathBuf>,
911
912 #[clap(long, hide(true))]
917 pub relay_console_path: Option<PathBuf>,
918
919 #[clap(long, hide(true))]
923 pub relay_console_title: Option<String>,
924
925 #[clap(long, value_name = "PORT")]
927 pub gdb: Option<u16>,
928
929 #[clap(long)]
934 pub mana: Vec<NicConfigCli>,
935
936 #[clap(long)]
951 pub hypervisor: Option<String>,
952
953 #[clap(long)]
959 pub nested_virt: bool,
960
961 #[clap(long_help = r#"
971e.g: --ide memdiff:file:/path/to/disk.vhd
972
973syntax: <path> | kind:<arg>[,flag,opt=arg,...]
974
975valid disk kinds:
976 `mem:<len>` memory backed disk
977 <len>: length of ramdisk, e.g.: `1G`
978 `memdiff:<disk>` memory backed diff disk
979 <disk>: lower disk, e.g.: `file:base.img`
980 `file:<path>[;create=<len>]` file-backed disk
981 <path>: path to file
982 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
983 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
984 `blob:<type>:<url>` HTTP blob (read-only)
985 <type>: `flat` or `vhd1`
986 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
987 <cipher>: `xts-aes-256`
988
989additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
990this list is not exhaustive.
991
992flags:
993 `ro` open disk as read-only
994 `s` attach drive to secondary ide channel
995 `dvd` specifies that device is cd/dvd and it is read_only
996"#)]
997 #[clap(long, value_name = "FILE", requires("pcat"))]
998 pub ide: Vec<IdeDiskCli>,
999
1000 #[clap(long_help = r#"
1003e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
1004
1005syntax: <path> | kind:<arg>[,flag,opt=arg,...]
1006
1007valid disk kinds:
1008 `mem:<len>` memory backed disk
1009 <len>: length of ramdisk, e.g.: `1G`
1010 `memdiff:<disk>` memory backed diff disk
1011 <disk>: lower disk, e.g.: `file:base.img`
1012 `file:<path>[;create=<len>]` file-backed disk
1013 <path>: path to file
1014 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
1015 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
1016 `blob:<type>:<url>` HTTP blob (read-only)
1017 <type>: `flat` or `vhd1`
1018 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
1019 <cipher>: `xts-aes-256`
1020
1021flags:
1022 `ro` open disk as read-only
1023"#)]
1024 #[clap(long, value_name = "FILE", requires("pcat"))]
1025 pub floppy: Vec<FloppyDiskCli>,
1026
1027 #[clap(long)]
1029 pub guest_watchdog: bool,
1030
1031 #[clap(long)]
1034 pub openhcl_dump_path: Option<PathBuf>,
1035
1036 #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action)]
1040 pub guest_reset_action: GuestPowerAction,
1041
1042 #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1046 pub guest_shutdown_action: GuestPowerAction,
1047
1048 #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1052 pub guest_crash_action: GuestPowerAction,
1053
1054 #[clap(long, value_name = "PATH")]
1062 pub crash_dump_path: Option<PathBuf>,
1063
1064 #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action, requires = "guest_watchdog")]
1068 pub guest_watchdog_action: GuestPowerAction,
1069
1070 #[clap(long)]
1072 pub write_saved_state_proto: Option<PathBuf>,
1073
1074 #[clap(long)]
1076 pub imc: Option<PathBuf>,
1077
1078 #[clap(long)]
1080 pub battery: bool,
1081
1082 #[clap(long)]
1084 pub uefi_console_mode: Option<UefiConsoleModeCli>,
1085
1086 #[clap(long_help = r#"
1088Set the EFI diagnostics log level.
1089
1090options:
1091 default default (ERROR and WARN only)
1092 info info (ERROR, WARN, and INFO)
1093 full full (all log levels)
1094"#)]
1095 #[clap(long, requires("uefi"))]
1096 pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
1097
1098 #[clap(long)]
1100 pub default_boot_always_attempt: bool,
1101
1102 #[cfg(guest_arch = "x86_64")]
1108 #[clap(long)]
1109 pub amd_iommu: Vec<String>,
1110
1111 #[cfg(guest_arch = "x86_64")]
1117 #[clap(long)]
1118 pub intel_vtd: Vec<String>,
1119
1120 #[clap(long_help = r#"
1122Attach root complexes to the VM.
1123
1124Examples:
1125 # Attach root complex rc0 on segment 0 with bus and MMIO ranges
1126 --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
1127
1128 # Configure HDM window size and restrictions (bitmask)
1129 --pcie-root-complex rc1,hdm=2G,hdm_window_restrictions=0x21
1130
1131Syntax: <name>[,opt=arg,...]
1132
1133Options:
1134 `segment=<value>` configures the PCI Express segment, default 0
1135 `start_bus=<value>` lowest valid bus number, default 0
1136 `end_bus=<value>` highest valid bus number, default 255
1137 `low_mmio=<size>` low MMIO window size, default 64M
1138 `high_mmio=<size>` high MMIO window size, default 1G
1139 `low_mmio_base=<addr>` pin low MMIO window base address (0x-prefixed hex)
1140 `high_mmio_base=<addr>` pin high MMIO window base address (0x-prefixed hex)
1141 `hdm=<size>` HDM decoder MMIO window size (CFMWS window), default 1G
1142 `hdm_window_restrictions=<m>` CFMWS window restriction bitmask (u16, decimal or 0x-prefixed hex),
1143 default DEVICE_COHERENT (bit 0, value 0x1)
1144 `preserve_bars` keep pinned BARs at their assigned addresses
1145 `node=<value>` NUMA node the root complex is associated with
1146"#)]
1147 #[clap(long, conflicts_with("pcat"))]
1148 pub pcie_root_complex: Vec<PcieRootComplexCli>,
1149
1150 #[clap(long_help = r#"
1152Attach root ports to root complexes.
1153
1154Examples:
1155 # Attach root port rc0rp0 to root complex rc0
1156 --pcie-root-port rc0:rc0rp0
1157
1158 # Attach root port rc0rp1 to root complex rc0 with hotplug support
1159 --pcie-root-port rc0:rc0rp1,hotplug
1160
1161 # Attach root port rc0rp2 at device 5, function 0
1162 --pcie-root-port rc0:rc0rp2,addr=5
1163
1164 # Attach root port rc0rp3 at device 5, function 1
1165 --pcie-root-port rc0:rc0rp3,addr=5.1
1166
1167Syntax: <root_complex_name>:<name>[,opt,opt=arg,...]
1168
1169Options:
1170 `addr=<dev>[.<fn>]` device/function to place this port at (default:
1171 lowest available); dev 0-31, fn 0-7
1172 `hotplug` enable hotplug support for this root port
1173 `acs=<mask>` ACS capability bitmask (u16, decimal or 0x-prefixed hex)
1174 `cxl` configure this root port as CXL-capable
1175 `pasid` configure this port to support PASID for downstream devices
1176"#)]
1177 #[clap(long, conflicts_with("pcat"))]
1178 pub pcie_root_port: Vec<PcieRootPortCli>,
1179
1180 #[clap(long_help = r#"
1182Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
1183
1184Examples:
1185 # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
1186 --pcie-switch rp0:switch0,num_downstream_ports=4
1187
1188 # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
1189 --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
1190
1191 # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
1192 --pcie-switch rp0:switch0
1193 --pcie-switch switch0-downstream-0:switch1
1194 --pcie-switch switch1-downstream-1:switch2
1195
1196 # Enable hotplug on all downstream switch ports of switch0
1197 --pcie-switch rp0:switch0,hotplug
1198
1199 # Enable PASID on all downstream switch ports of switch0
1200 --pcie-switch rp0:switch0,pasid
1201
1202Syntax: <port_name>:<name>[,opt,opt=arg,...]
1203
1204 port_name can be:
1205 - Root port name (e.g., "rp0") to connect directly to a root port
1206 - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
1207
1208Options:
1209 `hotplug` enable hotplug support for all downstream switch ports
1210 `num_downstream_ports=<value>` number of downstream ports, default 4
1211 `acs=<mask>` ACS capability bitmask for downstream switch ports
1212 `pasid` configure this port to support PASID for downstream devices
1213"#)]
1214 #[clap(long, conflicts_with("pcat"))]
1215 pub pcie_switch: Vec<GenericPcieSwitchCli>,
1216
1217 #[clap(long_help = r#"
1219Declare that the device directly behind a PCIe port is a generic initiator
1220(GI) for a NUMA node, generating an SRAT Generic Initiator Affinity structure.
1221
1222The port may be a root port or a switch downstream port, so this works for
1223devices that sit behind a switch (e.g. a GPU placed under a switch shared
1224with a NIC for peer-to-peer DMA). The port is resolved by name against the
1225live topology after switch downstream ports are enumerated.
1226
1227Examples:
1228 # The device behind switch downstream port sw1-downstream-0 is a generic
1229 # initiator for NUMA node 1
1230 --pcie-generic-initiator port=sw1-downstream-0,node=1
1231
1232 # Also works for a root port name
1233 --pcie-generic-initiator port=rp0,node=2
1234
1235Syntax: port=<port_name>,node=<node>
1236"#)]
1237 #[clap(
1238 long = "pcie-generic-initiator",
1239 value_name = "port=<name>,node=<node>",
1240 conflicts_with("pcat")
1241 )]
1242 pub pcie_generic_initiator: Vec<PcieGenericInitiatorCli>,
1243
1244 #[clap(long_help = r#"
1246Attach PCIe devices to root ports or downstream switch ports
1247which are implemented in a simulator running in a remote process.
1248
1249Examples:
1250 # Attach to root port rc0rp0 with default socket
1251 --pcie-remote rc0rp0
1252
1253 # Attach with custom socket address
1254 --pcie-remote rc0rp0,socket=0.0.0.0:48914
1255
1256 # Specify HU and controller identifiers
1257 --pcie-remote rc0rp0,hu=1,controller=0
1258
1259 # Multiple devices on different ports
1260 --pcie-remote rc0rp0,socket=0.0.0.0:48914
1261 --pcie-remote rc0rp1,socket=0.0.0.0:48915
1262
1263Syntax: <port_name>[,opt=arg,...]
1264
1265Options:
1266 `socket=<address>` TCP socket (default: localhost:48914)
1267 `hu=<value>` Hardware unit identifier (default: 0)
1268 `controller=<value>` Controller identifier (default: 0)
1269"#)]
1270 #[clap(long, conflicts_with("pcat"))]
1271 pub pcie_remote: Vec<PcieRemoteCli>,
1272
1273 #[clap(long_help = r#"
1275Assign a host PCI device to the guest via Linux VFIO.
1276
1277The device must be bound to vfio-pci on the host before starting the VM.
1278
1279Examples:
1280 --vfio host=0000:01:00.0,port=rp0
1281 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1282
1283Keys:
1284 host=<pci_bdf> (required) PCI address on the host
1285 port=<name> (required) Root port or downstream switch port name
1286 iommu=<id> (optional) Reference to an --iommu object. When present,
1287 uses VFIO cdev + iommufd instead of the legacy group path.
1288"#)]
1289 #[cfg(target_os = "linux")]
1290 #[clap(long, conflicts_with("pcat"))]
1291 pub vfio: Vec<VfioDeviceCli>,
1292
1293 #[clap(long_help = r#"
1295Declare an iommufd context. Opens /dev/iommu so it can be referenced by
1296--vfio devices via the iommu=<id> key. The associated IOAS is allocated
1297the first time a --vfio device referring to this id is opened.
1298
1299Requires Linux kernel >= 6.6 with iommufd support.
1300
1301Examples:
1302 --iommu id=iommu0 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1303
1304Syntax: id=<name>
1305"#)]
1306 #[cfg(target_os = "linux")]
1307 #[clap(long, conflicts_with("pcat"))]
1308 pub iommu: Vec<IommuCli>,
1309}
1310
1311impl Options {
1312 pub fn memory_size(&self) -> u64 {
1314 self.memory.size.map(|m| m.0).unwrap_or(DEFAULT_MEMORY_SIZE)
1315 }
1316
1317 pub fn prefetch_memory(&self) -> bool {
1319 self.memory.prefetch || self.deprecated_prefetch
1320 }
1321
1322 pub fn private_memory(&self) -> bool {
1324 self.memory.shared == Some(false) || self.deprecated_private_memory
1325 }
1326
1327 pub fn transparent_hugepages(&self) -> bool {
1329 self.memory
1330 .transparent_hugepages
1331 .unwrap_or(!self.memory.hugepages)
1332 || self.deprecated_thp
1333 }
1334
1335 pub fn memory_backing_file(&self) -> Option<&PathBuf> {
1337 self.memory
1338 .file
1339 .as_ref()
1340 .or(self.deprecated_memory_backing_file.as_ref())
1341 }
1342
1343 pub fn validate_memory_options(&self) -> anyhow::Result<()> {
1351 if self.memory.file.is_some() && self.deprecated_memory_backing_file.is_some() {
1352 anyhow::bail!("--memory file=... conflicts with --memory-backing-file");
1353 }
1354 if self.memory.file.is_some() && self.restore_snapshot.is_some() {
1355 anyhow::bail!("--memory file=... conflicts with --restore-snapshot");
1356 }
1357 if self.memory.shared == Some(true) && self.deprecated_private_memory {
1358 anyhow::bail!("--memory shared=on conflicts with --private-memory");
1359 }
1360 Ok(())
1361 }
1362}
1363
1364#[derive(Clone, Debug, PartialEq)]
1365pub struct FsArgs {
1366 pub tag: String,
1367 pub path: String,
1368 pub pcie_port: Option<String>,
1369}
1370
1371impl FromStr for FsArgs {
1372 type Err = anyhow::Error;
1373
1374 fn from_str(s: &str) -> Result<Self, Self::Err> {
1375 let (pcie_port, s) = parse_pcie_port_prefix(s);
1376 let mut s = s.split(',');
1377 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
1378 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
1379 };
1380 Ok(Self {
1381 tag: tag.to_owned(),
1382 path: path.to_owned(),
1383 pcie_port,
1384 })
1385 }
1386}
1387
1388#[derive(Clone, Debug, PartialEq)]
1389pub struct FsArgsWithOptions {
1390 pub tag: String,
1392 pub path: String,
1394 pub options: String,
1396 pub pcie_port: Option<String>,
1398}
1399
1400impl FromStr for FsArgsWithOptions {
1401 type Err = anyhow::Error;
1402
1403 fn from_str(s: &str) -> Result<Self, Self::Err> {
1404 let (pcie_port, s) = parse_pcie_port_prefix(s);
1405 let mut s = s.split(',');
1406 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
1407 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
1408 };
1409 let options = s.collect::<Vec<_>>().join(";");
1410 Ok(Self {
1411 tag: tag.to_owned(),
1412 path: path.to_owned(),
1413 options,
1414 pcie_port,
1415 })
1416 }
1417}
1418
1419#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1423pub enum GuestPowerAction {
1424 Reset,
1426 Halt,
1429 Exit(u8),
1431}
1432
1433fn parse_guest_power_action(s: &str) -> Result<GuestPowerAction, String> {
1436 match s {
1437 "reset" => Ok(GuestPowerAction::Reset),
1438 "halt" => Ok(GuestPowerAction::Halt),
1439 "exit" => Ok(GuestPowerAction::Exit(0)),
1440 _ => match s.strip_prefix("exit:") {
1441 Some(code) => code
1442 .parse::<u8>()
1443 .map(GuestPowerAction::Exit)
1444 .map_err(|err| format!("invalid exit code '{code}' (expected 0-255): {err}")),
1445 None => Err(format!(
1446 "expected reset, halt, exit, or exit:<code>, got '{s}'"
1447 )),
1448 },
1449 }
1450}
1451
1452#[derive(Copy, Clone, clap::ValueEnum)]
1453pub enum VirtioBusCli {
1454 Auto,
1455 Mmio,
1456 Pci,
1457 Vpci,
1458}
1459
1460#[cfg(target_os = "linux")]
1461fn parse_vhost_vsock_cid(value: &str) -> Result<u32, String> {
1462 let cid = value
1463 .parse::<u32>()
1464 .map_err(|error| format!("invalid CID '{value}': {error}"))?;
1465 if !(3..u32::MAX).contains(&cid) {
1466 return Err(format!("CID must be between 3 and {}", u32::MAX - 1));
1467 }
1468 Ok(cid)
1469}
1470
1471fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
1476 if let Some(rest) = s.strip_prefix("pcie_port=") {
1477 if let Some((port, rest)) = rest.split_once(':') {
1478 if !port.is_empty() {
1479 return (Some(port.to_string()), rest);
1480 }
1481 }
1482 }
1483 (None, s)
1484}
1485
1486#[derive(Clone, Debug, PartialEq)]
1487pub struct VirtioPmemArgs {
1488 pub path: String,
1489 pub pcie_port: Option<String>,
1490}
1491
1492impl FromStr for VirtioPmemArgs {
1493 type Err = anyhow::Error;
1494
1495 fn from_str(s: &str) -> Result<Self, Self::Err> {
1496 let (pcie_port, s) = parse_pcie_port_prefix(s);
1497 if s.is_empty() {
1498 anyhow::bail!("expected [pcie_port=<port>:]<path>");
1499 }
1500 Ok(Self {
1501 path: s.to_owned(),
1502 pcie_port,
1503 })
1504 }
1505}
1506
1507#[derive(clap::ValueEnum, Clone, Copy)]
1508pub enum SecureBootTemplateCli {
1509 Windows,
1510 UefiCa,
1511}
1512
1513fn parse_memory(s: &str) -> anyhow::Result<u64> {
1514 if s == "VMGS_DEFAULT" {
1515 Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
1516 } else {
1517 || -> Option<u64> {
1518 let mut b = s.as_bytes();
1519 if s.ends_with('B') {
1520 b = &b[..b.len() - 1]
1521 }
1522 if b.is_empty() {
1523 return None;
1524 }
1525 let multi = match b[b.len() - 1] as char {
1526 'T' => Some(1024 * 1024 * 1024 * 1024),
1527 'G' => Some(1024 * 1024 * 1024),
1528 'M' => Some(1024 * 1024),
1529 'K' => Some(1024),
1530 _ => None,
1531 };
1532 if multi.is_some() {
1533 b = &b[..b.len() - 1]
1534 }
1535 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
1536 n.checked_mul(multi.unwrap_or(1))
1537 }()
1538 .with_context(|| format!("invalid memory size '{0}'", s))
1539 }
1540}
1541
1542fn parse_address(s: &str) -> anyhow::Result<u64> {
1544 let hex = s
1545 .strip_prefix("0x")
1546 .or_else(|| s.strip_prefix("0X"))
1547 .with_context(|| format!("invalid address '{s}', expected a 0x-prefixed hex value"))?;
1548 u64::from_str_radix(hex, 16).with_context(|| format!("invalid address '{s}'"))
1549}
1550
1551fn parse_acs_capability_mask(value: &str) -> anyhow::Result<u16> {
1552 if let Some(hex) = value
1553 .strip_prefix("0x")
1554 .or_else(|| value.strip_prefix("0X"))
1555 {
1556 u16::from_str_radix(hex, 16).context("invalid ACS capability mask")
1557 } else {
1558 value.parse::<u16>().context("invalid ACS capability mask")
1559 }
1560}
1561
1562fn parse_memory_config(s: &str) -> anyhow::Result<MemoryCli> {
1563 let memory = if !s.contains('=') && !s.contains(',') {
1565 MemoryCli {
1566 size: Some(s.parse::<vmm_cli::MemorySize>()?),
1567 ..Default::default()
1568 }
1569 } else {
1570 s.parse::<MemoryCli>()?
1571 };
1572 memory.validate()?;
1573 Ok(memory)
1574}
1575
1576fn parse_numa_node(s: &str) -> anyhow::Result<NumaNodeCli> {
1577 let node: NumaNodeCli = s.parse()?;
1578 anyhow::ensure!(
1579 node.memory.size.is_some(),
1580 "numa node requires 'size' option"
1581 );
1582 anyhow::ensure!(
1583 node.memory.file.is_none(),
1584 "'file' is not supported in --numa"
1585 );
1586 node.memory.validate()?;
1587 Ok(node)
1588}
1589
1590fn parse_numa_distance(s: &str) -> anyhow::Result<NumaDistanceCli> {
1591 let parts: Vec<&str> = s.split(':').collect();
1592 anyhow::ensure!(
1593 parts.len() == 3,
1594 "expected SRC:DST:DISTANCE format, got '{s}'"
1595 );
1596 let src = parts[0].parse::<u32>().context("invalid source node")?;
1597 let dst = parts[1]
1598 .parse::<u32>()
1599 .context("invalid destination node")?;
1600 let distance = parts[2].parse::<u8>().context("invalid distance")?;
1601 anyhow::ensure!(
1602 distance >= 10,
1603 "distance must be >= 10 (10 = local), got {distance}"
1604 );
1605 Ok(NumaDistanceCli { src, dst, distance })
1606}
1607
1608fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
1610 match s.strip_prefix("0x") {
1611 Some(rest) => u64::from_str_radix(rest, 16),
1612 None => s.parse::<u64>(),
1613 }
1614}
1615
1616#[derive(Clone, Debug, PartialEq)]
1617pub enum DiskCliKind {
1618 Memory(u64),
1620 MemoryDiff(Box<DiskCliKind>),
1622 Sqlite {
1624 path: PathBuf,
1625 create_with_len: Option<u64>,
1626 },
1627 SqliteDiff {
1629 path: PathBuf,
1630 create: bool,
1631 disk: Box<DiskCliKind>,
1632 },
1633 AutoCacheSqlite {
1635 cache_path: String,
1636 key: Option<String>,
1637 disk: Box<DiskCliKind>,
1638 },
1639 PersistentReservationsWrapper(Box<DiskCliKind>),
1641 File {
1643 path: PathBuf,
1644 create_with_len: Option<u64>,
1645 direct: bool,
1646 },
1647 Blob {
1649 kind: BlobKind,
1650 url: String,
1651 },
1652 Crypt {
1654 cipher: DiskCipher,
1655 key_file: PathBuf,
1656 disk: Box<DiskCliKind>,
1657 },
1658 DelayDiskWrapper {
1660 delay_ms: u64,
1661 disk: Box<DiskCliKind>,
1662 },
1663}
1664
1665#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
1666pub enum DiskCipher {
1667 #[clap(name = "xts-aes-256")]
1668 XtsAes256,
1669}
1670
1671#[derive(Copy, Clone, Debug, PartialEq)]
1672pub enum BlobKind {
1673 Flat,
1674 Vhd1,
1675}
1676
1677struct FileOpts {
1678 path: PathBuf,
1679 create_with_len: Option<u64>,
1680 direct: bool,
1681}
1682
1683fn parse_file_opts(arg: &str) -> anyhow::Result<FileOpts> {
1684 let mut path = arg;
1685 let mut create_with_len = None;
1686 let mut direct = false;
1687
1688 if let Some((p, rest)) = arg.split_once(';') {
1690 path = p;
1691 for opt in rest.split(';') {
1692 if let Some(len) = opt.strip_prefix("create=") {
1693 create_with_len = Some(parse_memory(len)?);
1694 } else if opt == "direct" {
1695 direct = true;
1696 } else {
1697 anyhow::bail!("invalid file option '{opt}', expected 'create=<len>' or 'direct'");
1698 }
1699 }
1700 }
1701
1702 Ok(FileOpts {
1703 path: path.into(),
1704 create_with_len,
1705 direct,
1706 })
1707}
1708
1709impl DiskCliKind {
1710 fn parse_autocache(
1713 arg: &str,
1714 cache_path: Result<String, std::env::VarError>,
1715 ) -> anyhow::Result<Self> {
1716 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
1717 let cache_path = cache_path.context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
1718 Ok(DiskCliKind::AutoCacheSqlite {
1719 cache_path,
1720 key: (!key.is_empty()).then(|| key.to_string()),
1721 disk: Box::new(kind.parse()?),
1722 })
1723 }
1724}
1725
1726impl FromStr for DiskCliKind {
1727 type Err = anyhow::Error;
1728
1729 fn from_str(s: &str) -> anyhow::Result<Self> {
1730 let disk = match s.split_once(':') {
1731 None => {
1733 let FileOpts {
1734 path,
1735 create_with_len,
1736 direct,
1737 } = parse_file_opts(s)?;
1738 DiskCliKind::File {
1739 path,
1740 create_with_len,
1741 direct,
1742 }
1743 }
1744 Some((kind, arg)) => match kind {
1745 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
1746 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
1747 "sql" => {
1748 let FileOpts {
1749 path,
1750 create_with_len,
1751 direct,
1752 } = parse_file_opts(arg)?;
1753 if direct {
1754 anyhow::bail!("'direct' is not supported for 'sql' disks");
1755 }
1756 DiskCliKind::Sqlite {
1757 path,
1758 create_with_len,
1759 }
1760 }
1761 "sqldiff" => {
1762 let (path_and_opts, kind) =
1763 arg.split_once(':').context("expected path[;opts]:kind")?;
1764 let disk = Box::new(kind.parse()?);
1765 match path_and_opts.split_once(';') {
1766 Some((path, create)) => {
1767 if create != "create" {
1768 anyhow::bail!("invalid syntax after ';', expected 'create'")
1769 }
1770 DiskCliKind::SqliteDiff {
1771 path: path.into(),
1772 create: true,
1773 disk,
1774 }
1775 }
1776 None => DiskCliKind::SqliteDiff {
1777 path: path_and_opts.into(),
1778 create: false,
1779 disk,
1780 },
1781 }
1782 }
1783 "autocache" => {
1784 Self::parse_autocache(arg, std::env::var("OPENVMM_AUTO_CACHE_PATH"))?
1785 }
1786 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
1787 "file" => {
1788 let FileOpts {
1789 path,
1790 create_with_len,
1791 direct,
1792 } = parse_file_opts(arg)?;
1793 DiskCliKind::File {
1794 path,
1795 create_with_len,
1796 direct,
1797 }
1798 }
1799 "blob" => {
1800 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
1801 let blob_kind = match blob_kind {
1802 "flat" => BlobKind::Flat,
1803 "vhd1" => BlobKind::Vhd1,
1804 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
1805 };
1806 DiskCliKind::Blob {
1807 kind: blob_kind,
1808 url: url.to_string(),
1809 }
1810 }
1811 "crypt" => {
1812 let (cipher, (key, kind)) = arg
1813 .split_once(':')
1814 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
1815 .context("expected cipher:key_file:kind")?;
1816 DiskCliKind::Crypt {
1817 cipher: ValueEnum::from_str(cipher, false)
1818 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
1819 key_file: PathBuf::from(key),
1820 disk: Box::new(kind.parse()?),
1821 }
1822 }
1823 kind => {
1824 let FileOpts {
1829 path,
1830 create_with_len,
1831 direct,
1832 } = parse_file_opts(s)?;
1833 if path.has_root() {
1834 DiskCliKind::File {
1835 path,
1836 create_with_len,
1837 direct,
1838 }
1839 } else {
1840 anyhow::bail!("invalid disk kind {kind}");
1841 }
1842 }
1843 },
1844 };
1845 Ok(disk)
1846 }
1847}
1848
1849#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1851pub enum RpcTransportCli {
1852 #[default]
1855 Auto,
1856 Ttrpc,
1858 Grpc,
1860}
1861
1862#[derive(Clone, Debug, PartialEq, Eq)]
1864pub struct RpcCli {
1865 pub path: PathBuf,
1867 pub transport: RpcTransportCli,
1869}
1870
1871impl FromStr for RpcCli {
1872 type Err = anyhow::Error;
1873
1874 fn from_str(s: &str) -> anyhow::Result<Self> {
1875 let mut path = None;
1876 let mut transport = None;
1877 for part in s.split(',') {
1878 let (key, value) = part
1879 .split_once('=')
1880 .with_context(|| format!("invalid rpc option '{part}', expected key=value"))?;
1881 match key {
1882 "path" => {
1883 anyhow::ensure!(path.is_none(), "duplicate option 'path'");
1884 anyhow::ensure!(!value.is_empty(), "'path' requires a value");
1885 path = Some(PathBuf::from(value));
1886 }
1887 "transport" => {
1888 anyhow::ensure!(transport.is_none(), "duplicate option 'transport'");
1889 transport = Some(match value {
1890 "auto" => RpcTransportCli::Auto,
1891 "ttrpc" => RpcTransportCli::Ttrpc,
1892 "grpc" => RpcTransportCli::Grpc,
1893 _ => anyhow::bail!(
1894 "invalid transport '{value}', expected auto, ttrpc, or grpc"
1895 ),
1896 });
1897 }
1898 _ => anyhow::bail!("unknown rpc option '{key}'"),
1899 }
1900 }
1901
1902 Ok(RpcCli {
1903 path: path.context("'path' is required")?,
1904 transport: transport.unwrap_or_default(),
1905 })
1906 }
1907}
1908
1909#[derive(Clone)]
1910pub struct VmgsCli {
1911 pub kind: DiskCliKind,
1912 pub provision: ProvisionVmgs,
1913}
1914
1915#[derive(Copy, Clone)]
1916pub enum ProvisionVmgs {
1917 OnEmpty,
1918 OnFailure,
1919 True,
1920}
1921
1922impl FromStr for VmgsCli {
1923 type Err = anyhow::Error;
1924
1925 fn from_str(s: &str) -> anyhow::Result<Self> {
1926 let (kind, opt) = s
1927 .split_once(',')
1928 .map(|(k, o)| (k, Some(o)))
1929 .unwrap_or((s, None));
1930 let kind = kind.parse()?;
1931
1932 let provision = match opt {
1933 None => ProvisionVmgs::OnEmpty,
1934 Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
1935 Some("fmt") => ProvisionVmgs::True,
1936 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
1937 };
1938
1939 Ok(VmgsCli { kind, provision })
1940 }
1941}
1942
1943#[derive(clap::Args)]
1945pub struct VncCli {
1946 #[clap(long)]
1948 pub vnc: bool,
1949
1950 #[clap(long, value_name = "PORT", default_value = "5900")]
1952 pub vnc_port: u16,
1953
1954 #[clap(long, value_name = "ADDRESS", default_value = "127.0.0.1")]
1958 pub vnc_listen: String,
1959
1960 #[clap(long, value_name = "COUNT", default_value = "16")]
1962 pub vnc_max_clients: usize,
1963
1964 #[clap(long)]
1967 pub vnc_evict_oldest: bool,
1968}
1969
1970#[derive(Clone)]
1972pub struct DiskCli {
1973 pub vtl: DeviceVtl,
1974 pub kind: DiskCliKind,
1975 pub read_only: bool,
1976 pub is_dvd: bool,
1977 pub underhill: Option<UnderhillDiskSource>,
1978 pub pcie_port: Option<String>,
1979 pub controller: Option<String>,
1980 pub nsid: Option<u32>,
1981 pub lun: Option<u8>,
1982 pub relay: Option<(String, Option<u32>)>,
1983}
1984
1985#[derive(Copy, Clone)]
1986pub enum UnderhillDiskSource {
1987 Scsi,
1988 Nvme,
1989}
1990
1991struct RelayTarget {
1993 name: String,
1994 location: Option<u32>,
1995}
1996
1997impl FromStr for RelayTarget {
1998 type Err = anyhow::Error;
1999
2000 fn from_str(s: &str) -> anyhow::Result<Self> {
2001 Ok(if let Some((name, loc)) = s.split_once(':') {
2002 RelayTarget {
2003 name: name.to_string(),
2004 location: Some(loc.parse::<u32>().context("invalid relay location")?),
2005 }
2006 } else {
2007 RelayTarget {
2008 name: s.to_string(),
2009 location: None,
2010 }
2011 })
2012 }
2013}
2014
2015#[derive(vmm_cli::KeyValueArgs)]
2018struct DiskArgs {
2019 #[kv(positional)]
2020 kind: DiskCliKind,
2021 #[kv(flag)]
2022 ro: bool,
2023 #[kv(flag)]
2024 dvd: bool,
2025 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2026 vtl: DeviceVtl,
2027 #[kv(flag)]
2028 uh: bool,
2029 #[kv(flag, key = "uh-nvme")]
2030 uh_nvme: bool,
2031 pcie_port: Option<String>,
2032 #[kv(key = "on")]
2033 controller: Option<String>,
2034 nsid: Option<u32>,
2035 lun: Option<u8>,
2036 relay: Option<RelayTarget>,
2037}
2038
2039impl FromStr for DiskCli {
2040 type Err = anyhow::Error;
2041
2042 fn from_str(s: &str) -> anyhow::Result<Self> {
2043 let args: DiskArgs = s.parse()?;
2044
2045 let underhill = match (args.uh, args.uh_nvme) {
2046 (false, false) => None,
2047 (true, false) => Some(UnderhillDiskSource::Scsi),
2048 (false, true) => Some(UnderhillDiskSource::Nvme),
2049 (true, true) => anyhow::bail!("`uh` and `uh-nvme` are mutually exclusive"),
2050 };
2051 let read_only = args.ro || args.dvd;
2052 let is_dvd = args.dvd;
2053 let vtl = args.vtl;
2054 let pcie_port = args.pcie_port;
2055 let controller = args.controller;
2056 let nsid = args.nsid;
2057 let lun = args.lun;
2058 let relay = args.relay.map(|r| (r.name, r.location));
2059
2060 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
2061 anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
2062 }
2063
2064 if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
2065 anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
2066 }
2067
2068 if controller.is_some() && pcie_port.is_some() {
2069 anyhow::bail!("`on` is incompatible with `pcie_port`");
2070 }
2071
2072 if controller.is_some() && vtl != DeviceVtl::Vtl0 {
2073 anyhow::bail!(
2074 "`vtl2` is incompatible with `on`; the controller's VTL determines placement"
2075 );
2076 }
2077
2078 if controller.is_some() && underhill.is_some() {
2079 anyhow::bail!("`on` is incompatible with `uh` and `uh-nvme`; use `relay` instead");
2080 }
2081
2082 if nsid.is_some() && controller.is_none() {
2083 anyhow::bail!("`nsid` requires `on`");
2084 }
2085
2086 if lun.is_some() && controller.is_none() {
2087 anyhow::bail!("`lun` requires `on`");
2088 }
2089
2090 if nsid.is_some() && lun.is_some() {
2091 anyhow::bail!("`nsid` and `lun` are mutually exclusive");
2092 }
2093
2094 if relay.is_some() && controller.is_none() {
2095 anyhow::bail!("`relay` requires `on`");
2096 }
2097
2098 if relay.is_some() && underhill.is_some() {
2099 anyhow::bail!("`relay` is incompatible with `uh` and `uh-nvme`");
2100 }
2101
2102 Ok(DiskCli {
2103 vtl,
2104 kind: args.kind,
2105 read_only,
2106 is_dvd,
2107 underhill,
2108 pcie_port,
2109 controller,
2110 nsid,
2111 lun,
2112 relay,
2113 })
2114 }
2115}
2116
2117#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueGroup)]
2119pub enum NvmeControllerTransport {
2120 #[kv(key = "pcie_port")]
2122 Pcie(String),
2123 #[kv(key = "vpci")]
2125 Vpci(Option<Guid>),
2126}
2127
2128#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2130pub struct NvmeControllerCli {
2131 pub id: String,
2133 #[kv(flatten)]
2135 pub transport: NvmeControllerTransport,
2136 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2138 pub vtl: DeviceVtl,
2139}
2140
2141#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2143pub struct ScsiControllerCli {
2144 pub id: String,
2146 #[kv(default)]
2148 pub sub_channels: u16,
2149 #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2151 pub vtl: DeviceVtl,
2152}
2153
2154#[derive(Copy, Clone, Debug, PartialEq)]
2156pub enum OpenhclControllerType {
2157 Scsi,
2158 Nvme,
2159}
2160
2161#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2163pub struct OpenhclControllerCli {
2164 pub id: String,
2166 #[kv(key = "type")]
2168 pub controller_type: OpenhclControllerType,
2169 pub guid: Option<Guid>,
2171}
2172
2173impl FromStr for OpenhclControllerType {
2174 type Err = anyhow::Error;
2175
2176 fn from_str(s: &str) -> anyhow::Result<Self> {
2177 Ok(match s {
2178 "scsi" => OpenhclControllerType::Scsi,
2179 "nvme" => OpenhclControllerType::Nvme,
2180 other => anyhow::bail!("unknown controller type: '{other}'"),
2181 })
2182 }
2183}
2184
2185#[derive(Clone, Debug, PartialEq)]
2187pub struct CxlTestDeviceCli {
2188 pub hdm_size: u64,
2190 pub pcie_port: String,
2192}
2193
2194impl FromStr for CxlTestDeviceCli {
2195 type Err = anyhow::Error;
2196
2197 fn from_str(s: &str) -> anyhow::Result<Self> {
2198 let mut opts = s.split(',');
2199 let first = opts.next().context("expected CXL test device config")?;
2200 let (kind, arg) = first
2201 .split_once(':')
2202 .context("expected CXL test syntax: mem:<len>")?;
2203
2204 if kind != "mem" {
2205 anyhow::bail!("unsupported CXL test backing kind '{kind}', expected 'mem'");
2206 }
2207
2208 let hdm_size = parse_memory(arg).context("failed to parse CXL test HDM size")?;
2209 let mut pcie_port = None;
2210
2211 for opt in opts {
2212 let mut kv = opt.split('=');
2213 let key = kv.next().unwrap_or_default();
2214 match key {
2215 "pcie_port" => {
2216 let val = kv.next();
2217 if val.is_none_or(|v| v.is_empty()) {
2218 anyhow::bail!("`pcie_port` requires a port name");
2219 }
2220 pcie_port = Some(val.unwrap().to_string());
2221 }
2222 _ => anyhow::bail!("unknown option: '{opt}'"),
2223 }
2224 }
2225
2226 let Some(pcie_port) = pcie_port else {
2227 anyhow::bail!("`pcie_port=<name>` is required for `--cxl-test`");
2228 };
2229
2230 Ok(Self {
2231 hdm_size,
2232 pcie_port,
2233 })
2234 }
2235}
2236
2237#[derive(Clone)]
2239pub struct IdeDiskCli {
2240 pub kind: DiskCliKind,
2241 pub read_only: bool,
2242 pub channel: Option<u8>,
2243 pub device: Option<u8>,
2244 pub is_dvd: bool,
2245}
2246
2247impl FromStr for IdeDiskCli {
2248 type Err = anyhow::Error;
2249
2250 fn from_str(s: &str) -> anyhow::Result<Self> {
2251 let mut opts = s.split(',');
2252 let kind = opts.next().unwrap().parse()?;
2253
2254 let mut read_only = false;
2255 let mut channel = None;
2256 let mut device = None;
2257 let mut is_dvd = false;
2258 for opt in opts {
2259 let mut s = opt.split('=');
2260 let opt = s.next().unwrap();
2261 match opt {
2262 "ro" => read_only = true,
2263 "p" => channel = Some(0),
2264 "s" => channel = Some(1),
2265 "0" => device = Some(0),
2266 "1" => device = Some(1),
2267 "dvd" => {
2268 is_dvd = true;
2269 read_only = true;
2270 }
2271 _ => anyhow::bail!("unknown option: '{opt}'"),
2272 }
2273 }
2274
2275 Ok(IdeDiskCli {
2276 kind,
2277 read_only,
2278 channel,
2279 device,
2280 is_dvd,
2281 })
2282 }
2283}
2284
2285#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
2287pub struct FloppyDiskCli {
2288 #[kv(positional)]
2289 pub kind: DiskCliKind,
2290 #[kv(flag, key = "ro")]
2291 pub read_only: bool,
2292}
2293
2294#[derive(Clone)]
2295pub struct DebugconSerialConfigCli {
2296 pub port: u16,
2297 pub serial: SerialConfigCli,
2298}
2299
2300impl FromStr for DebugconSerialConfigCli {
2301 type Err = String;
2302
2303 fn from_str(s: &str) -> Result<Self, Self::Err> {
2304 let Some((port, serial)) = s.split_once(',') else {
2305 return Err("invalid format (missing comma between port and serial)".into());
2306 };
2307
2308 let port: u16 = parse_number(port)
2309 .map_err(|_| "could not parse port".to_owned())?
2310 .try_into()
2311 .map_err(|_| "port must be 16-bit")?;
2312 let serial: SerialConfigCli = serial.parse()?;
2313
2314 Ok(Self { port, serial })
2315 }
2316}
2317
2318#[derive(Clone, Debug, PartialEq)]
2321pub struct ComSerialConfigCli {
2322 pub debugger_mode: bool,
2324 pub backend: SerialConfigCli,
2326}
2327
2328impl FromStr for ComSerialConfigCli {
2329 type Err = String;
2330
2331 fn from_str(s: &str) -> Result<Self, Self::Err> {
2332 match s.strip_prefix("debugger-mode:") {
2333 Some(rest) => Ok(Self {
2334 debugger_mode: true,
2335 backend: rest.parse()?,
2336 }),
2337 None => Ok(Self {
2338 debugger_mode: false,
2339 backend: s.parse()?,
2340 }),
2341 }
2342 }
2343}
2344
2345#[derive(Clone, Debug, PartialEq)]
2347pub enum SerialConfigCli {
2348 None,
2349 Console,
2350 NewConsole(Option<PathBuf>, Option<String>),
2351 Stderr,
2352 Pipe(PathBuf),
2353 Tcp(SocketAddr),
2354 File(PathBuf),
2355}
2356
2357impl FromStr for SerialConfigCli {
2358 type Err = String;
2359
2360 fn from_str(s: &str) -> Result<Self, Self::Err> {
2361 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
2362
2363 let first_key = match keyvalues.first() {
2364 Some(first_pair) => first_pair.0.as_str(),
2365 None => Err("invalid serial configuration: no values supplied")?,
2366 };
2367 let first_value = keyvalues.first().unwrap().1.as_ref();
2368
2369 let ret = match first_key {
2370 "none" => SerialConfigCli::None,
2371 "console" => SerialConfigCli::Console,
2372 "stderr" => SerialConfigCli::Stderr,
2373 "file" => match first_value {
2374 Some(path) => SerialConfigCli::File(path.into()),
2375 None => Err("invalid serial configuration: file requires a value")?,
2376 },
2377 "term" => {
2378 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
2380 let window_name = match window_name {
2381 Some((_, Some(name))) => Some(name.clone()),
2382 _ => None,
2383 };
2384
2385 SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
2386 }
2387 "listen" => match first_value {
2388 Some(path) => {
2389 if let Some(tcp) = path.strip_prefix("tcp:") {
2390 let addr = tcp
2391 .parse()
2392 .map_err(|err| format!("invalid tcp address: {err}"))?;
2393 SerialConfigCli::Tcp(addr)
2394 } else {
2395 SerialConfigCli::Pipe(path.into())
2396 }
2397 }
2398 None => Err(
2399 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
2400 )?,
2401 },
2402 _ => {
2403 return Err(format!(
2404 "invalid serial configuration: '{}' is not a known option",
2405 first_key
2406 ));
2407 }
2408 };
2409
2410 Ok(ret)
2411 }
2412}
2413
2414impl SerialConfigCli {
2415 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
2418 let mut ret = Vec::new();
2419
2420 for item in s.split(',') {
2422 let mut eqsplit = item.split('=');
2425 let key = eqsplit.next();
2426 let value = eqsplit.next();
2427
2428 if let Some(key) = key {
2429 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
2430 } else {
2431 return Err("invalid key=value pair in serial config".into());
2433 }
2434 }
2435 Ok(ret)
2436 }
2437}
2438
2439#[derive(Clone, Debug, PartialEq)]
2440pub enum EndpointConfigCli {
2441 None,
2442 Consomme {
2443 cidr: Option<String>,
2444 host_fwd: Vec<HostPortConfigCli>,
2445 },
2446 Dio {
2447 id: Option<String>,
2448 },
2449 Tap {
2450 name: String,
2451 },
2452}
2453
2454#[derive(Clone, Debug, PartialEq)]
2456pub struct HostPortConfigCli {
2457 pub protocol: HostPortProtocolCli,
2458 pub host_address: Option<std::net::IpAddr>,
2459 pub host_port: u16,
2460 pub guest_port: u16,
2461}
2462
2463#[derive(Clone, Debug, PartialEq)]
2465pub enum HostPortProtocolCli {
2466 Tcp,
2467 Udp,
2468}
2469
2470fn parse_hostfwd(s: &str) -> Result<HostPortConfigCli, String> {
2471 let (host_part, guest_part) = s.split_once('-').ok_or_else(|| {
2474 format!(
2475 "invalid hostfwd format '{s}', \
2476 expected 'proto:[hostaddr]:hostport-[guestaddr]:guestport'"
2477 )
2478 })?;
2479
2480 let (proto, host_addr_port) = host_part.split_once(':').ok_or_else(|| {
2482 format!("invalid hostfwd host part '{host_part}', expected 'proto:[hostaddr]:hostport'")
2483 })?;
2484 let protocol = match proto {
2485 "tcp" => HostPortProtocolCli::Tcp,
2486 "udp" => HostPortProtocolCli::Udp,
2487 other => {
2488 return Err(format!(
2489 "unknown hostfwd protocol '{other}', expected 'tcp' or 'udp'"
2490 ));
2491 }
2492 };
2493
2494 let (host_address, host_port) = parse_addr_port(host_addr_port)
2495 .map_err(|e| format!("invalid hostfwd host address/port: {e}"))?;
2496 let (_, guest_port) = parse_addr_port(guest_part)
2497 .map_err(|e| format!("invalid hostfwd guest address/port: {e}"))?;
2498
2499 Ok(HostPortConfigCli {
2500 protocol,
2501 host_address,
2502 host_port,
2503 guest_port,
2504 })
2505}
2506
2507fn parse_addr_port(s: &str) -> Result<(Option<std::net::IpAddr>, u16), String> {
2513 if let Some(rest) = s.strip_prefix('[') {
2514 let (addr, port) = rest
2516 .split_once("]:")
2517 .ok_or_else(|| format!("expected '[addr]:port', got '[{rest}'"))?;
2518 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
2519 let addr: std::net::IpAddr = addr
2520 .parse()
2521 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
2522 Ok((Some(addr), port))
2523 } else {
2524 match s.rsplit_once(':') {
2525 Some((addr, port)) => {
2526 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
2527 let addr = if addr.is_empty() {
2528 None
2529 } else {
2530 let parsed: std::net::IpAddr = addr
2531 .parse()
2532 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
2533 Some(parsed)
2534 };
2535 Ok((addr, port))
2536 }
2537 None => {
2538 let port: u16 = s.parse().map_err(|_| format!("invalid port '{s}'"))?;
2539 Ok((None, port))
2540 }
2541 }
2542 }
2543}
2544
2545impl FromStr for EndpointConfigCli {
2546 type Err = String;
2547
2548 fn from_str(s: &str) -> Result<Self, Self::Err> {
2549 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
2550 ["none"] => EndpointConfigCli::None,
2551 ["consomme", rest @ ..] => {
2552 let remaining = rest.join(":");
2553 let mut cidr = None;
2554 let mut host_fwd = Vec::new();
2555 for opt in remaining.split(',').filter(|s| !s.is_empty()) {
2556 if let Some(fwd) = opt.strip_prefix("hostfwd=") {
2557 host_fwd.push(parse_hostfwd(fwd)?);
2558 } else if cidr.is_none() {
2559 cidr = Some(opt.to_owned());
2560 } else {
2561 return Err(format!("unexpected consomme option '{opt}'"));
2562 }
2563 }
2564 EndpointConfigCli::Consomme { cidr, host_fwd }
2565 }
2566 ["dio", s @ ..] => EndpointConfigCli::Dio {
2567 id: s.first().map(|s| (*s).to_owned()),
2568 },
2569 ["tap", name] => EndpointConfigCli::Tap {
2570 name: (*name).to_owned(),
2571 },
2572 _ => return Err("invalid network backend".into()),
2573 };
2574
2575 Ok(ret)
2576 }
2577}
2578
2579#[derive(Clone, Debug, PartialEq)]
2580pub struct NicConfigCli {
2581 pub vtl: DeviceVtl,
2582 pub endpoint: EndpointConfigCli,
2583 pub max_queues: Option<u16>,
2584 pub underhill: bool,
2585 pub pcie_port: Option<String>,
2586}
2587
2588impl FromStr for NicConfigCli {
2589 type Err = String;
2590
2591 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
2592 let mut vtl = DeviceVtl::Vtl0;
2593 let mut max_queues = None;
2594 let mut underhill = false;
2595 let mut pcie_port = None;
2596 while let Some((opt, rest)) = s.split_once(':') {
2597 if let Some((opt, val)) = opt.split_once('=') {
2598 match opt {
2599 "queues" => {
2600 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
2601 }
2602 "pcie_port" => {
2603 if val.is_empty() {
2604 return Err("`pcie_port=` requires port name argument".into());
2605 }
2606 pcie_port = Some(val.to_string());
2607 }
2608 _ => break,
2609 }
2610 } else {
2611 match opt {
2612 "vtl2" => {
2613 vtl = DeviceVtl::Vtl2;
2614 }
2615 "uh" => underhill = true,
2616 _ => break,
2617 }
2618 }
2619 s = rest;
2620 }
2621
2622 if underhill && vtl != DeviceVtl::Vtl0 {
2623 return Err("`uh` is incompatible with `vtl2`".into());
2624 }
2625
2626 if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
2627 return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
2628 }
2629
2630 let endpoint = s.parse()?;
2631 Ok(NicConfigCli {
2632 vtl,
2633 endpoint,
2634 max_queues,
2635 underhill,
2636 pcie_port,
2637 })
2638 }
2639}
2640
2641#[derive(Debug, Error)]
2642#[error("unknown VTL2 relocation type: {0}")]
2643pub struct UnknownVtl2RelocationType(String);
2644
2645fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
2646 match s {
2647 "disable" => Ok(Vtl2BaseAddressType::File),
2648 s if s.starts_with("auto=") => {
2649 let s = s.strip_prefix("auto=").unwrap_or_default();
2650 let size = if s == "filesize" {
2651 None
2652 } else {
2653 let size = parse_memory(s).map_err(|e| {
2654 UnknownVtl2RelocationType(format!(
2655 "unable to parse memory size from {} for 'auto=' type, {e}",
2656 e
2657 ))
2658 })?;
2659 Some(size)
2660 };
2661 Ok(Vtl2BaseAddressType::MemoryLayout { size })
2662 }
2663 s if s.starts_with("absolute=") => {
2664 let s = s.strip_prefix("absolute=");
2665 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
2666 UnknownVtl2RelocationType(format!(
2667 "unable to parse number from {} for 'absolute=' type",
2668 e
2669 ))
2670 })?;
2671 Ok(Vtl2BaseAddressType::Absolute(addr))
2672 }
2673 s if s.starts_with("vtl2=") => {
2674 let s = s.strip_prefix("vtl2=").unwrap_or_default();
2675 let size = if s == "filesize" {
2676 None
2677 } else {
2678 let size = parse_memory(s).map_err(|e| {
2679 UnknownVtl2RelocationType(format!(
2680 "unable to parse memory size from {} for 'vtl2=' type, {e}",
2681 e
2682 ))
2683 })?;
2684 Some(size)
2685 };
2686 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
2687 }
2688 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
2689 }
2690}
2691
2692#[derive(Debug, Copy, Clone, PartialEq)]
2693pub enum SmtConfigCli {
2694 Auto,
2695 Force,
2696 Off,
2697}
2698
2699#[derive(Debug, Error)]
2700#[error("expected auto, force, or off")]
2701pub struct BadSmtConfig;
2702
2703impl FromStr for SmtConfigCli {
2704 type Err = BadSmtConfig;
2705
2706 fn from_str(s: &str) -> Result<Self, Self::Err> {
2707 let r = match s {
2708 "auto" => Self::Auto,
2709 "force" => Self::Force,
2710 "off" => Self::Off,
2711 _ => return Err(BadSmtConfig),
2712 };
2713 Ok(r)
2714 }
2715}
2716
2717#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
2718fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
2719 let r = match s {
2720 "auto" => X2ApicConfig::Auto,
2721 "supported" => X2ApicConfig::Supported,
2722 "off" => X2ApicConfig::Unsupported,
2723 "on" => X2ApicConfig::Enabled,
2724 _ => return Err("expected auto, supported, off, or on"),
2725 };
2726 Ok(r)
2727}
2728
2729#[derive(Debug, Copy, Clone, ValueEnum)]
2730pub enum Vtl0LateMapPolicyCli {
2731 Off,
2732 Log,
2733 Halt,
2734 Exception,
2735}
2736
2737#[derive(Debug, Copy, Clone, Default, ValueEnum)]
2739pub enum GicMsiCli {
2740 #[default]
2742 Auto,
2743 Its,
2745 V2m,
2747}
2748
2749#[derive(Debug, Copy, Clone, ValueEnum)]
2750pub enum IsolationCli {
2751 Vbs,
2752}
2753
2754#[derive(Debug, Copy, Clone, PartialEq)]
2755pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
2756
2757impl FromStr for PcatBootOrderCli {
2758 type Err = &'static str;
2759
2760 fn from_str(s: &str) -> Result<Self, Self::Err> {
2761 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
2762 let mut order = Vec::new();
2763
2764 for item in s.split(',') {
2765 let device = match item {
2766 "optical" => PcatBootDevice::Optical,
2767 "hdd" => PcatBootDevice::HardDrive,
2768 "net" => PcatBootDevice::Network,
2769 "floppy" => PcatBootDevice::Floppy,
2770 _ => return Err("unknown boot device type"),
2771 };
2772
2773 let default_pos = default_order
2774 .iter()
2775 .position(|x| x == &Some(device))
2776 .ok_or("cannot pass duplicate boot devices")?;
2777
2778 order.push(default_order[default_pos].take().unwrap());
2779 }
2780
2781 order.extend(default_order.into_iter().flatten());
2782 assert_eq!(order.len(), 4);
2783
2784 Ok(Self(order.try_into().unwrap()))
2785 }
2786}
2787
2788#[derive(Copy, Clone, Debug, ValueEnum)]
2789pub enum UefiConsoleModeCli {
2790 Default,
2791 Com1,
2792 Com2,
2793 None,
2794}
2795
2796#[derive(Copy, Clone, Debug, Default, ValueEnum)]
2797pub enum EfiDiagnosticsLogLevelCli {
2798 #[default]
2799 Default,
2800 Info,
2801 Full,
2802}
2803
2804#[derive(Clone, Debug, PartialEq)]
2805pub struct PcieRootComplexCli {
2806 pub name: String,
2807 pub segment: u16,
2808 pub start_bus: u8,
2809 pub end_bus: u8,
2810 pub low_mmio: u32,
2811 pub high_mmio: u64,
2812 pub low_mmio_base: Option<u64>,
2813 pub high_mmio_base: Option<u64>,
2814 pub preserve_bars: bool,
2815 pub hdm: u64,
2816 pub hdm_window_restrictions: CfmwsWindowRestrictions,
2817 pub vnode: Option<u32>,
2818}
2819
2820struct HexAddress(u64);
2822
2823impl FromStr for HexAddress {
2824 type Err = anyhow::Error;
2825
2826 fn from_str(s: &str) -> anyhow::Result<Self> {
2827 Ok(HexAddress(parse_address(s)?))
2828 }
2829}
2830
2831struct CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions);
2833
2834impl FromStr for CfmwsWindowRestrictionsCli {
2835 type Err = anyhow::Error;
2836
2837 fn from_str(s: &str) -> anyhow::Result<Self> {
2838 Ok(CfmwsWindowRestrictionsCli(
2839 parse_cxl_cfmws_window_restriction_u16_bitmask(s)?,
2840 ))
2841 }
2842}
2843
2844#[derive(vmm_cli::KeyValueArgs)]
2847struct PcieRootComplexArgs {
2848 #[kv(positional)]
2849 name: String,
2850 #[kv(default)]
2851 segment: u16,
2852 #[kv(default)]
2853 start_bus: u8,
2854 #[kv(default = 255)]
2855 end_bus: u8,
2856 #[kv(default = vmm_cli::MemorySize(64 * 1024 * 1024))]
2857 low_mmio: vmm_cli::MemorySize,
2858 #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
2859 high_mmio: vmm_cli::MemorySize,
2860 low_mmio_base: Option<HexAddress>,
2861 high_mmio_base: Option<HexAddress>,
2862 #[kv(flag)]
2863 preserve_bars: bool,
2864 #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
2865 hdm: vmm_cli::MemorySize,
2866 #[kv(default = CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions::DEVICE_COHERENT))]
2867 hdm_window_restrictions: CfmwsWindowRestrictionsCli,
2868 #[kv(key = "node")]
2869 vnode: Option<u32>,
2870}
2871
2872impl FromStr for PcieRootComplexCli {
2873 type Err = anyhow::Error;
2874
2875 fn from_str(s: &str) -> Result<Self, Self::Err> {
2876 let args: PcieRootComplexArgs = s.parse()?;
2877
2878 if args.start_bus > args.end_bus {
2879 anyhow::bail!("start_bus must be <= end_bus");
2880 }
2881
2882 let low_mmio = u32::try_from(args.low_mmio.0).context("low MMIO size exceeds 32 bits")?;
2883
2884 Ok(PcieRootComplexCli {
2885 name: args.name,
2886 segment: args.segment,
2887 start_bus: args.start_bus,
2888 end_bus: args.end_bus,
2889 low_mmio,
2890 high_mmio: args.high_mmio.0,
2891 low_mmio_base: args.low_mmio_base.map(|a| a.0),
2892 high_mmio_base: args.high_mmio_base.map(|a| a.0),
2893 preserve_bars: args.preserve_bars,
2894 hdm: args.hdm.0,
2895 hdm_window_restrictions: args.hdm_window_restrictions.0,
2896 vnode: args.vnode,
2897 })
2898 }
2899}
2900
2901fn parse_cxl_cfmws_window_restriction_u16_bitmask(
2902 s: &str,
2903) -> anyhow::Result<CfmwsWindowRestrictions> {
2904 let bits = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
2905 u16::from_str_radix(hex, 16).context("invalid hex bitmask")?
2906 } else {
2907 u16::from_str(s).context("invalid decimal bitmask")?
2908 };
2909
2910 CfmwsWindowRestrictions::try_from_bits(bits)
2911 .context("bitmask includes reserved CFMWS window restriction bits")
2912}
2913
2914#[derive(Clone, Debug, PartialEq)]
2915pub struct PcieRootPortCli {
2916 pub root_complex_name: String,
2917 pub name: String,
2918 pub devfn: Option<u8>,
2919 pub hotplug: bool,
2920 pub acs_capabilities_supported: Option<u16>,
2921 pub cxl: bool,
2922 pub pasid: bool,
2923}
2924
2925struct PortNamePair {
2928 parent: String,
2929 child: String,
2930}
2931
2932impl FromStr for PortNamePair {
2933 type Err = anyhow::Error;
2934
2935 fn from_str(s: &str) -> anyhow::Result<Self> {
2936 let mut it = s.split(':');
2937 let parent = it
2938 .next()
2939 .filter(|x| !x.is_empty())
2940 .context("expected parent name")?;
2941 let child = it
2942 .next()
2943 .filter(|x| !x.is_empty())
2944 .context("expected child name")?;
2945 anyhow::ensure!(it.next().is_none(), "unexpected token in '{s}'");
2946 Ok(PortNamePair {
2947 parent: parent.to_string(),
2948 child: child.to_string(),
2949 })
2950 }
2951}
2952
2953struct PcieAddr(u8);
2955
2956impl FromStr for PcieAddr {
2957 type Err = anyhow::Error;
2958
2959 fn from_str(s: &str) -> anyhow::Result<Self> {
2960 Ok(PcieAddr(parse_pcie_addr(s)?))
2961 }
2962}
2963
2964struct AcsMask(u16);
2966
2967impl FromStr for AcsMask {
2968 type Err = anyhow::Error;
2969
2970 fn from_str(s: &str) -> anyhow::Result<Self> {
2971 Ok(AcsMask(parse_acs_capability_mask(s)?))
2972 }
2973}
2974
2975#[derive(vmm_cli::KeyValueArgs)]
2977struct RootPortArgs {
2978 #[kv(positional)]
2979 names: PortNamePair,
2980 addr: Option<PcieAddr>,
2981 #[kv(flag)]
2982 hotplug: bool,
2983 acs: Option<AcsMask>,
2984 #[kv(flag)]
2985 cxl: bool,
2986 #[kv(flag)]
2987 pasid: bool,
2988}
2989
2990impl FromStr for PcieRootPortCli {
2991 type Err = anyhow::Error;
2992
2993 fn from_str(s: &str) -> Result<Self, Self::Err> {
2994 let args: RootPortArgs = s.parse()?;
2995 Ok(PcieRootPortCli {
2996 root_complex_name: args.names.parent,
2997 name: args.names.child,
2998 devfn: args.addr.map(|a| a.0),
2999 hotplug: args.hotplug,
3000 acs_capabilities_supported: args.acs.map(|a| a.0),
3001 cxl: args.cxl,
3002 pasid: args.pasid,
3003 })
3004 }
3005}
3006
3007fn parse_pcie_addr(s: &str) -> anyhow::Result<u8> {
3011 let parse_int = |v: &str| -> anyhow::Result<u8> {
3012 if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) {
3013 u8::from_str_radix(hex, 16).context("invalid hex number")
3014 } else {
3015 v.parse().context("invalid number")
3016 }
3017 };
3018
3019 let mut parts = s.split('.');
3020 let device = parse_int(parts.next().context("expected device number")?)?;
3021 let function = match parts.next() {
3022 Some(f) => parse_int(f)?,
3023 None => 0,
3024 };
3025 if parts.next().is_some() {
3026 anyhow::bail!("unexpected token in addr '{s}'");
3027 }
3028 if device > 31 {
3029 anyhow::bail!("device number {device} out of range (0-31)");
3030 }
3031 if function > 7 {
3032 anyhow::bail!("function number {function} out of range (0-7)");
3033 }
3034 Ok((device << 3) | function)
3035}
3036
3037#[derive(Clone, Debug, PartialEq)]
3038pub struct GenericPcieSwitchCli {
3039 pub port_name: String,
3040 pub name: String,
3041 pub num_downstream_ports: u8,
3042 pub hotplug: bool,
3043 pub acs_capabilities_supported: Option<u16>,
3044 pub pasid: bool,
3045}
3046
3047#[derive(vmm_cli::KeyValueArgs)]
3049struct SwitchArgs {
3050 #[kv(positional)]
3051 names: PortNamePair,
3052 #[kv(default = 4)]
3053 num_downstream_ports: u8,
3054 #[kv(flag)]
3055 hotplug: bool,
3056 acs: Option<AcsMask>,
3057 #[kv(flag)]
3058 pasid: bool,
3059}
3060
3061impl FromStr for GenericPcieSwitchCli {
3062 type Err = anyhow::Error;
3063
3064 fn from_str(s: &str) -> Result<Self, Self::Err> {
3065 let args: SwitchArgs = s.parse()?;
3066 Ok(GenericPcieSwitchCli {
3067 port_name: args.names.parent,
3068 name: args.names.child,
3069 num_downstream_ports: args.num_downstream_ports,
3070 hotplug: args.hotplug,
3071 acs_capabilities_supported: args.acs.map(|a| a.0),
3072 pasid: args.pasid,
3073 })
3074 }
3075}
3076
3077#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3079pub struct PcieGenericInitiatorCli {
3080 #[kv(key = "port")]
3083 pub port_name: String,
3084 pub node: u32,
3086}
3087
3088#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3090pub struct PcieRemoteCli {
3091 #[kv(positional)]
3093 pub port_name: String,
3094 #[kv(key = "socket")]
3096 pub socket_addr: Option<String>,
3097 #[kv(default)]
3099 pub hu: u16,
3100 #[kv(default)]
3102 pub controller: u16,
3103}
3104
3105#[cfg(target_os = "linux")]
3109#[derive(Clone, Debug)]
3110pub struct VfioDeviceCli {
3111 pub port_name: String,
3113 pub pci_id: String,
3115 pub iommu: Option<String>,
3118 pub bar_addresses: [vfio_assigned_device_resources::BarAddressConfig; 6],
3120}
3121
3122#[cfg(target_os = "linux")]
3124struct BarAddressCli(vfio_assigned_device_resources::BarAddressConfig);
3125
3126#[cfg(target_os = "linux")]
3127impl FromStr for BarAddressCli {
3128 type Err = anyhow::Error;
3129
3130 fn from_str(s: &str) -> anyhow::Result<Self> {
3131 let config = if s == "host" {
3132 vfio_assigned_device_resources::BarAddressConfig::HostAssigned
3133 } else if let Some(value) = s.strip_prefix("0x") {
3134 let address = u64::from_str_radix(value, 16).context("invalid BAR address")?;
3135 anyhow::ensure!(address != 0, "BAR address must be nonzero");
3136 vfio_assigned_device_resources::BarAddressConfig::Fixed(address)
3137 } else {
3138 anyhow::bail!("expected 'host' or a hexadecimal address starting with '0x'");
3139 };
3140 Ok(Self(config))
3141 }
3142}
3143
3144#[cfg(target_os = "linux")]
3147#[derive(vmm_cli::KeyValueArgs)]
3148struct BarFlags {
3149 bar0: Option<BarAddressCli>,
3150 bar1: Option<BarAddressCli>,
3151 bar2: Option<BarAddressCli>,
3152 bar3: Option<BarAddressCli>,
3153 bar4: Option<BarAddressCli>,
3154 bar5: Option<BarAddressCli>,
3155}
3156
3157#[cfg(target_os = "linux")]
3159#[derive(vmm_cli::KeyValueArgs)]
3160struct VfioArgs {
3161 host: String,
3162 port: String,
3163 iommu: Option<String>,
3164 #[kv(flatten)]
3165 bars: BarFlags,
3166}
3167
3168#[cfg(target_os = "linux")]
3169impl FromStr for VfioDeviceCli {
3170 type Err = anyhow::Error;
3171
3172 fn from_str(s: &str) -> Result<Self, Self::Err> {
3173 let args: VfioArgs = s.parse()?;
3174
3175 if args.host.contains('/') || args.host.contains("..") {
3177 anyhow::bail!("PCI address must not contain path separators");
3178 }
3179
3180 let bars = args.bars;
3181 let bar_addresses = [
3182 bars.bar0.map(|bar| bar.0).unwrap_or_default(),
3183 bars.bar1.map(|bar| bar.0).unwrap_or_default(),
3184 bars.bar2.map(|bar| bar.0).unwrap_or_default(),
3185 bars.bar3.map(|bar| bar.0).unwrap_or_default(),
3186 bars.bar4.map(|bar| bar.0).unwrap_or_default(),
3187 bars.bar5.map(|bar| bar.0).unwrap_or_default(),
3188 ];
3189
3190 Ok(VfioDeviceCli {
3191 port_name: args.port,
3192 pci_id: args.host,
3193 iommu: args.iommu,
3194 bar_addresses,
3195 })
3196 }
3197}
3198
3199#[cfg(guest_arch = "aarch64")]
3203#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
3204pub struct SmmuCli {
3205 #[kv(key = "rc")]
3207 pub rc_name: String,
3208 #[kv(flag)]
3210 pub accel: bool,
3211 #[kv(default)]
3213 pub oas: SmmuOasCli,
3214}
3215
3216#[cfg(guest_arch = "aarch64")]
3218#[derive(Clone, Copy, Debug, Default)]
3219pub enum SmmuOasCli {
3220 #[default]
3223 Auto,
3224 Fixed(u8),
3226}
3227
3228#[cfg(guest_arch = "aarch64")]
3229impl FromStr for SmmuOasCli {
3230 type Err = anyhow::Error;
3231
3232 fn from_str(s: &str) -> Result<Self, Self::Err> {
3233 Ok(if s == "auto" {
3234 SmmuOasCli::Auto
3235 } else {
3236 SmmuOasCli::Fixed(s.parse().context("oas must be 'auto' or a number")?)
3237 })
3238 }
3239}
3240
3241#[cfg(target_os = "linux")]
3245#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
3246pub struct IommuCli {
3247 pub id: String,
3249}
3250
3251fn default_value_from_arch_env(name: &str) -> OsString {
3259 let prefix = if cfg!(guest_arch = "x86_64") {
3260 "X86_64"
3261 } else if cfg!(guest_arch = "aarch64") {
3262 "AARCH64"
3263 } else {
3264 return Default::default();
3265 };
3266 let prefixed = format!("{}_{}", prefix, name);
3267 std::env::var_os(name)
3268 .or_else(|| std::env::var_os(prefixed))
3269 .unwrap_or_default()
3270}
3271
3272#[derive(Clone)]
3274pub struct OptionalPathBuf(pub Option<PathBuf>);
3275
3276impl From<&std::ffi::OsStr> for OptionalPathBuf {
3277 fn from(s: &std::ffi::OsStr) -> Self {
3278 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
3279 }
3280}
3281
3282#[cfg(target_os = "linux")]
3283#[derive(Clone)]
3284pub enum VhostUserDeviceTypeCli {
3285 Blk {
3288 num_queues: Option<u16>,
3289 queue_size: Option<u16>,
3290 },
3291 Fs {
3293 tag: String,
3294 num_queues: Option<u16>,
3295 queue_size: Option<u16>,
3296 },
3297 Other {
3299 device_id: u16,
3300 queue_sizes: Vec<u16>,
3301 },
3302}
3303
3304#[cfg(target_os = "linux")]
3305#[derive(Clone)]
3306pub struct VhostUserCli {
3307 pub socket_path: String,
3308 pub device_type: VhostUserDeviceTypeCli,
3309 pub pcie_port: Option<String>,
3310}
3311
3312#[cfg(target_os = "linux")]
3315#[derive(vmm_cli::KeyValueArgs)]
3316struct VhostUserArgs {
3317 #[kv(positional)]
3318 socket_path: String,
3319 #[kv(key = "type")]
3320 type_name: Option<String>,
3321 device_id: Option<u16>,
3322 tag: Option<String>,
3323 pcie_port: Option<String>,
3324 num_queues: Option<u16>,
3325 queue_size: Option<u16>,
3326 queue_sizes: Option<vmm_cli::BracketList<u16>>,
3327}
3328
3329#[cfg(target_os = "linux")]
3330impl FromStr for VhostUserCli {
3331 type Err = anyhow::Error;
3332
3333 fn from_str(s: &str) -> anyhow::Result<Self> {
3334 let mut args: VhostUserArgs = s.parse()?;
3335 let type_name = args.type_name.take();
3336
3337 if type_name.is_some() == args.device_id.is_some() {
3338 anyhow::bail!("must specify type=<name> or device_id=<N>");
3339 }
3340
3341 let device_type = match type_name.as_deref() {
3344 Some("fs") => VhostUserDeviceTypeCli::Fs {
3345 tag: args.tag.take().context("type=fs requires tag=<name>")?,
3346 num_queues: args.num_queues.take(),
3347 queue_size: args.queue_size.take(),
3348 },
3349 Some("blk") => VhostUserDeviceTypeCli::Blk {
3350 num_queues: args.num_queues.take(),
3351 queue_size: args.queue_size.take(),
3352 },
3353 Some(ty) => anyhow::bail!("unknown vhost-user device type: '{ty}'"),
3354 None => {
3355 let queue_sizes = args
3356 .queue_sizes
3357 .take()
3358 .context("device_id= requires queue_sizes=[N,N,...]")?
3359 .0;
3360 anyhow::ensure!(!queue_sizes.is_empty(), "queue_sizes must be non-empty");
3361 VhostUserDeviceTypeCli::Other {
3362 device_id: args.device_id.unwrap(),
3363 queue_sizes,
3364 }
3365 }
3366 };
3367
3368 if args.tag.is_some() {
3369 anyhow::bail!("tag= is only valid for type=fs");
3370 }
3371 if args.queue_sizes.is_some() {
3372 anyhow::bail!("queue_sizes= is only valid for device_id=");
3373 }
3374 if args.num_queues.is_some() || args.queue_size.is_some() {
3375 anyhow::bail!(
3376 "num_queues= and queue_size= are not valid for device_id=; use queue_sizes="
3377 );
3378 }
3379
3380 Ok(VhostUserCli {
3381 socket_path: args.socket_path,
3382 device_type,
3383 pcie_port: args.pcie_port,
3384 })
3385 }
3386}
3387
3388#[cfg(test)]
3389mod tests {
3390 use super::*;
3391
3392 use std::path::Path;
3393 use test_with_tracing::test;
3394
3395 #[test]
3396 fn test_parse_rpc() {
3397 let rpc = RpcCli::from_str("path=/tmp/openvmm.sock").unwrap();
3399 assert_eq!(rpc.path, Path::new("/tmp/openvmm.sock"));
3400 assert_eq!(rpc.transport, RpcTransportCli::Auto);
3401
3402 for (s, transport) in [
3404 ("auto", RpcTransportCli::Auto),
3405 ("ttrpc", RpcTransportCli::Ttrpc),
3406 ("grpc", RpcTransportCli::Grpc),
3407 ] {
3408 let rpc = RpcCli::from_str(&format!("path=/tmp/s.sock,transport={s}")).unwrap();
3409 assert_eq!(rpc.path, Path::new("/tmp/s.sock"));
3410 assert_eq!(rpc.transport, transport);
3411 }
3412
3413 assert!(RpcCli::from_str("").is_err());
3415 assert!(RpcCli::from_str("transport=ttrpc").is_err());
3416 assert!(RpcCli::from_str("path=").is_err());
3417 assert!(RpcCli::from_str("path=/tmp/s.sock,transport=bogus").is_err());
3418 assert!(RpcCli::from_str("path=/tmp/s.sock,bogus=1").is_err());
3419 assert!(RpcCli::from_str("path=/a,path=/b").is_err());
3420 }
3421
3422 #[test]
3423 fn test_parse_file_opts() {
3424 let disk = DiskCliKind::from_str("file:test.vhd;create=1G").unwrap();
3426 assert!(matches!(
3427 &disk,
3428 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
3429 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
3430 ));
3431
3432 let disk = DiskCliKind::from_str("test.vhd;create=1G").unwrap();
3434 assert!(matches!(
3435 &disk,
3436 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
3437 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
3438 ));
3439
3440 let disk = DiskCliKind::from_str("file:/dev/sdb;direct").unwrap();
3442 assert!(matches!(
3443 &disk,
3444 DiskCliKind::File { path, create_with_len: None, direct: true }
3445 if path == Path::new("/dev/sdb")
3446 ));
3447
3448 let disk = DiskCliKind::from_str("file:disk.img;direct;create=1G").unwrap();
3450 assert!(matches!(
3451 &disk,
3452 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
3453 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
3454 ));
3455
3456 let disk = DiskCliKind::from_str("file:disk.img;create=1G;direct").unwrap();
3457 assert!(matches!(
3458 &disk,
3459 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
3460 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
3461 ));
3462
3463 let disk = DiskCliKind::from_str("file:disk.img").unwrap();
3465 assert!(matches!(
3466 &disk,
3467 DiskCliKind::File { path, create_with_len: None, direct: false }
3468 if path == Path::new("disk.img")
3469 ));
3470
3471 assert!(DiskCliKind::from_str("file:disk.img;bogus").is_err());
3473
3474 assert!(DiskCliKind::from_str("sql:db.sqlite;direct").is_err());
3476 }
3477
3478 #[test]
3479 fn test_parse_memory_disk() {
3480 let s = "mem:1G";
3481 let disk = DiskCliKind::from_str(s).unwrap();
3482 match disk {
3483 DiskCliKind::Memory(size) => {
3484 assert_eq!(size, 1024 * 1024 * 1024); }
3486 _ => panic!("Expected Memory variant"),
3487 }
3488 }
3489
3490 #[test]
3491 fn test_parse_pcie_disk() {
3492 assert_eq!(
3493 DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
3494 Some("p0".to_string())
3495 );
3496 assert_eq!(
3497 DiskCli::from_str("file:path.vhdx,pcie_port=p0")
3498 .unwrap()
3499 .pcie_port,
3500 Some("p0".to_string())
3501 );
3502 assert_eq!(
3503 DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
3504 .unwrap()
3505 .pcie_port,
3506 Some("p0".to_string())
3507 );
3508
3509 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
3511
3512 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
3514 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
3515 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
3516 }
3517
3518 #[test]
3519 fn test_parse_memory_diff_disk() {
3520 let s = "memdiff:file:base.img";
3521 let disk = DiskCliKind::from_str(s).unwrap();
3522 match disk {
3523 DiskCliKind::MemoryDiff(inner) => match *inner {
3524 DiskCliKind::File {
3525 path,
3526 create_with_len,
3527 ..
3528 } => {
3529 assert_eq!(path, PathBuf::from("base.img"));
3530 assert_eq!(create_with_len, None);
3531 }
3532 _ => panic!("Expected File variant inside MemoryDiff"),
3533 },
3534 _ => panic!("Expected MemoryDiff variant"),
3535 }
3536 }
3537
3538 #[test]
3539 fn test_parse_sqlite_disk() {
3540 let s = "sql:db.sqlite;create=2G";
3541 let disk = DiskCliKind::from_str(s).unwrap();
3542 match disk {
3543 DiskCliKind::Sqlite {
3544 path,
3545 create_with_len,
3546 } => {
3547 assert_eq!(path, PathBuf::from("db.sqlite"));
3548 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
3549 }
3550 _ => panic!("Expected Sqlite variant"),
3551 }
3552
3553 let s = "sql:db.sqlite";
3555 let disk = DiskCliKind::from_str(s).unwrap();
3556 match disk {
3557 DiskCliKind::Sqlite {
3558 path,
3559 create_with_len,
3560 } => {
3561 assert_eq!(path, PathBuf::from("db.sqlite"));
3562 assert_eq!(create_with_len, None);
3563 }
3564 _ => panic!("Expected Sqlite variant"),
3565 }
3566 }
3567
3568 #[test]
3569 fn test_parse_sqlite_diff_disk() {
3570 let s = "sqldiff:diff.sqlite;create:file:base.img";
3572 let disk = DiskCliKind::from_str(s).unwrap();
3573 match disk {
3574 DiskCliKind::SqliteDiff { path, create, disk } => {
3575 assert_eq!(path, PathBuf::from("diff.sqlite"));
3576 assert!(create);
3577 match *disk {
3578 DiskCliKind::File {
3579 path,
3580 create_with_len,
3581 ..
3582 } => {
3583 assert_eq!(path, PathBuf::from("base.img"));
3584 assert_eq!(create_with_len, None);
3585 }
3586 _ => panic!("Expected File variant inside SqliteDiff"),
3587 }
3588 }
3589 _ => panic!("Expected SqliteDiff variant"),
3590 }
3591
3592 let s = "sqldiff:diff.sqlite:file:base.img";
3594 let disk = DiskCliKind::from_str(s).unwrap();
3595 match disk {
3596 DiskCliKind::SqliteDiff { path, create, disk } => {
3597 assert_eq!(path, PathBuf::from("diff.sqlite"));
3598 assert!(!create);
3599 match *disk {
3600 DiskCliKind::File {
3601 path,
3602 create_with_len,
3603 ..
3604 } => {
3605 assert_eq!(path, PathBuf::from("base.img"));
3606 assert_eq!(create_with_len, None);
3607 }
3608 _ => panic!("Expected File variant inside SqliteDiff"),
3609 }
3610 }
3611 _ => panic!("Expected SqliteDiff variant"),
3612 }
3613 }
3614
3615 #[test]
3616 fn test_parse_autocache_sqlite_disk() {
3617 let disk =
3619 DiskCliKind::parse_autocache(":file:disk.vhd", Ok("/tmp/cache".to_string())).unwrap();
3620 assert!(matches!(
3621 disk,
3622 DiskCliKind::AutoCacheSqlite {
3623 cache_path,
3624 key,
3625 disk: _disk,
3626 } if cache_path == "/tmp/cache" && key.is_none()
3627 ));
3628
3629 let disk =
3631 DiskCliKind::parse_autocache("mykey:file:disk.vhd", Ok("/tmp/cache".to_string()))
3632 .unwrap();
3633 assert!(matches!(
3634 disk,
3635 DiskCliKind::AutoCacheSqlite {
3636 cache_path,
3637 key: Some(key),
3638 disk: _disk,
3639 } if cache_path == "/tmp/cache" && key == "mykey"
3640 ));
3641
3642 assert!(
3644 DiskCliKind::parse_autocache(":file:disk.vhd", Err(std::env::VarError::NotPresent),)
3645 .is_err()
3646 );
3647 }
3648
3649 #[test]
3650 fn test_parse_disk_errors() {
3651 assert!(DiskCliKind::from_str("invalid:").is_err());
3652 assert!(DiskCliKind::from_str("memory:extra").is_err());
3653
3654 assert!(DiskCliKind::from_str("sqlite:").is_err());
3656 }
3657
3658 #[test]
3659 fn test_parse_errors() {
3660 assert!(DiskCliKind::from_str("mem:invalid").is_err());
3662
3663 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
3665
3666 assert!(
3668 DiskCliKind::parse_autocache("key:file:disk.vhd", Err(std::env::VarError::NotPresent),)
3669 .is_err()
3670 );
3671
3672 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
3674
3675 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
3677
3678 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
3680
3681 assert!(DiskCliKind::from_str("invalid:path").is_err());
3683
3684 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
3686 }
3687
3688 #[test]
3689 fn test_fs_args_from_str() {
3690 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
3691 assert_eq!(args.tag, "tag1");
3692 assert_eq!(args.path, "/path/to/fs");
3693
3694 assert!(FsArgs::from_str("tag1").is_err());
3696 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
3697 }
3698
3699 #[test]
3700 fn test_fs_args_with_options_from_str() {
3701 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
3702 assert_eq!(args.tag, "tag1");
3703 assert_eq!(args.path, "/path/to/fs");
3704 assert_eq!(args.options, "opt1;opt2");
3705
3706 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
3708 assert_eq!(args.tag, "tag1");
3709 assert_eq!(args.path, "/path/to/fs");
3710 assert_eq!(args.options, "");
3711
3712 assert!(FsArgsWithOptions::from_str("tag1").is_err());
3714 }
3715
3716 #[test]
3717 fn test_serial_config_from_str() {
3718 assert_eq!(
3719 SerialConfigCli::from_str("none").unwrap(),
3720 SerialConfigCli::None
3721 );
3722 assert_eq!(
3723 SerialConfigCli::from_str("console").unwrap(),
3724 SerialConfigCli::Console
3725 );
3726 assert_eq!(
3727 SerialConfigCli::from_str("stderr").unwrap(),
3728 SerialConfigCli::Stderr
3729 );
3730
3731 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
3733 if let SerialConfigCli::File(path) = file_config {
3734 assert_eq!(path.to_str().unwrap(), "/path/to/file");
3735 } else {
3736 panic!("Expected File variant");
3737 }
3738
3739 match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
3741 SerialConfigCli::NewConsole(None, Some(name)) => {
3742 assert_eq!(name, "MyTerm");
3743 }
3744 _ => panic!("Expected NewConsole variant with name"),
3745 }
3746
3747 match SerialConfigCli::from_str("term").unwrap() {
3749 SerialConfigCli::NewConsole(None, None) => (),
3750 _ => panic!("Expected NewConsole variant without name"),
3751 }
3752
3753 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
3755 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
3756 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
3757 assert_eq!(name, "MyTerm");
3758 }
3759 _ => panic!("Expected NewConsole variant with name"),
3760 }
3761
3762 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
3764 SerialConfigCli::NewConsole(Some(path), None) => {
3765 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
3766 }
3767 _ => panic!("Expected NewConsole variant without name"),
3768 }
3769
3770 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
3772 SerialConfigCli::Tcp(addr) => {
3773 assert_eq!(addr.to_string(), "127.0.0.1:1234");
3774 }
3775 _ => panic!("Expected Tcp variant"),
3776 }
3777
3778 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
3780 SerialConfigCli::Pipe(path) => {
3781 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
3782 }
3783 _ => panic!("Expected Pipe variant"),
3784 }
3785
3786 assert!(SerialConfigCli::from_str("").is_err());
3788 assert!(SerialConfigCli::from_str("unknown").is_err());
3789 assert!(SerialConfigCli::from_str("file").is_err());
3790 assert!(SerialConfigCli::from_str("listen").is_err());
3791 }
3792
3793 #[test]
3794 fn test_endpoint_config_from_str() {
3795 assert!(matches!(
3797 EndpointConfigCli::from_str("none").unwrap(),
3798 EndpointConfigCli::None
3799 ));
3800
3801 match EndpointConfigCli::from_str("consomme").unwrap() {
3803 EndpointConfigCli::Consomme {
3804 cidr: None,
3805 host_fwd,
3806 } => assert!(host_fwd.is_empty()),
3807 _ => panic!("Expected Consomme variant without cidr"),
3808 }
3809
3810 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
3812 EndpointConfigCli::Consomme {
3813 cidr: Some(cidr),
3814 host_fwd,
3815 } => {
3816 assert_eq!(cidr, "192.168.0.0/24");
3817 assert!(host_fwd.is_empty());
3818 }
3819 _ => panic!("Expected Consomme variant with cidr"),
3820 }
3821
3822 match EndpointConfigCli::from_str("consomme:hostfwd=udp:127.0.0.1:5000-:5000").unwrap() {
3824 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3825 assert!(cidr.is_none());
3826 assert_eq!(host_fwd.len(), 1);
3827 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Udp);
3828 assert_eq!(
3829 host_fwd[0].host_address,
3830 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
3831 );
3832 assert_eq!(host_fwd[0].host_port, 5000);
3833 assert_eq!(host_fwd[0].guest_port, 5000);
3834 }
3835 _ => panic!("Expected Consomme variant with hostfwd"),
3836 }
3837
3838 match EndpointConfigCli::from_str("consomme:10.0.0.0/24,hostfwd=tcp::2222-:22").unwrap() {
3840 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3841 assert_eq!(cidr.as_deref(), Some("10.0.0.0/24"));
3842 assert_eq!(host_fwd.len(), 1);
3843 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3844 assert_eq!(host_fwd[0].host_port, 2222);
3845 assert_eq!(host_fwd[0].guest_port, 22);
3846 }
3847 _ => panic!("Expected Consomme variant with cidr and hostfwd"),
3848 }
3849
3850 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::2222-:22,hostfwd=tcp::3389-:3389")
3852 .unwrap()
3853 {
3854 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3855 assert!(cidr.is_none());
3856 assert_eq!(host_fwd.len(), 2);
3857 assert_eq!(host_fwd[0].host_port, 2222);
3858 assert_eq!(host_fwd[0].guest_port, 22);
3859 assert_eq!(host_fwd[1].host_port, 3389);
3860 assert_eq!(host_fwd[1].guest_port, 3389);
3861 }
3862 _ => panic!("Expected Consomme variant with multiple hostfwd"),
3863 }
3864
3865 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:127.0.0.1:8080-:80").unwrap() {
3867 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3868 assert!(cidr.is_none());
3869 assert_eq!(host_fwd.len(), 1);
3870 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3871 assert_eq!(
3872 host_fwd[0].host_address,
3873 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
3874 );
3875 assert_eq!(host_fwd[0].host_port, 8080);
3876 assert_eq!(host_fwd[0].guest_port, 80);
3877 }
3878 _ => panic!("Expected Consomme variant with host/guest port mapping"),
3879 }
3880
3881 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-10.0.0.2:80").unwrap() {
3883 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3884 assert!(cidr.is_none());
3885 assert_eq!(host_fwd[0].host_port, 8080);
3886 assert_eq!(host_fwd[0].guest_port, 80);
3887 }
3888 _ => panic!("Expected Consomme variant with guest address"),
3889 }
3890
3891 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:[::1]:8080-:80").unwrap() {
3893 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3894 assert!(cidr.is_none());
3895 assert_eq!(host_fwd.len(), 1);
3896 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3897 assert_eq!(
3898 host_fwd[0].host_address,
3899 Some(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST))
3900 );
3901 assert_eq!(host_fwd[0].host_port, 8080);
3902 assert_eq!(host_fwd[0].guest_port, 80);
3903 }
3904 _ => panic!("Expected Consomme variant with IPv6 hostfwd"),
3905 }
3906
3907 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-[::1]:80").unwrap() {
3909 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3910 assert!(cidr.is_none());
3911 assert_eq!(host_fwd[0].host_port, 8080);
3912 assert_eq!(host_fwd[0].guest_port, 80);
3913 }
3914 _ => panic!("Expected Consomme variant with IPv6 guest address"),
3915 }
3916
3917 match EndpointConfigCli::from_str("dio").unwrap() {
3919 EndpointConfigCli::Dio { id: None } => (),
3920 _ => panic!("Expected Dio variant without id"),
3921 }
3922
3923 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
3925 EndpointConfigCli::Dio { id: Some(id) } => {
3926 assert_eq!(id, "test_id");
3927 }
3928 _ => panic!("Expected Dio variant with id"),
3929 }
3930
3931 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
3933 EndpointConfigCli::Tap { name } => {
3934 assert_eq!(name, "tap0");
3935 }
3936 _ => panic!("Expected Tap variant"),
3937 }
3938
3939 assert!(EndpointConfigCli::from_str("invalid").is_err());
3941 }
3942
3943 #[test]
3944 fn test_nic_config_from_str() {
3945 use openvmm_defs::config::DeviceVtl;
3946
3947 let config = NicConfigCli::from_str("none").unwrap();
3949 assert_eq!(config.vtl, DeviceVtl::Vtl0);
3950 assert!(config.max_queues.is_none());
3951 assert!(!config.underhill);
3952 assert!(config.pcie_port.is_none());
3953 assert!(matches!(config.endpoint, EndpointConfigCli::None));
3954
3955 let config = NicConfigCli::from_str("vtl2:none").unwrap();
3957 assert_eq!(config.vtl, DeviceVtl::Vtl2);
3958 assert!(config.pcie_port.is_none());
3959 assert!(matches!(config.endpoint, EndpointConfigCli::None));
3960
3961 let config = NicConfigCli::from_str("queues=4:none").unwrap();
3963 assert_eq!(config.max_queues, Some(4));
3964 assert!(config.pcie_port.is_none());
3965 assert!(matches!(config.endpoint, EndpointConfigCli::None));
3966
3967 let config = NicConfigCli::from_str("uh:none").unwrap();
3969 assert!(config.underhill);
3970 assert!(config.pcie_port.is_none());
3971 assert!(matches!(config.endpoint, EndpointConfigCli::None));
3972
3973 let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
3975 assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
3976 assert!(matches!(config.endpoint, EndpointConfigCli::None));
3977
3978 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
3980 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
3982 assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
3983 assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
3984 assert!(NicConfigCli::from_str("pcie_port:none").is_err());
3985 }
3986
3987 #[test]
3988 fn test_parse_pcie_port_prefix() {
3989 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
3991 assert_eq!(port.unwrap(), "rp0");
3992 assert_eq!(rest, "tag,path");
3993
3994 let (port, rest) = parse_pcie_port_prefix("tag,path");
3996 assert!(port.is_none());
3997 assert_eq!(rest, "tag,path");
3998
3999 let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
4001 assert!(port.is_none());
4002 assert_eq!(rest, "pcie_port=:tag,path");
4003
4004 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
4006 assert!(port.is_none());
4007 assert_eq!(rest, "pcie_port=rp0");
4008 }
4009
4010 #[test]
4011 fn test_cxl_test_device_cli_parse_valid() {
4012 let cfg = CxlTestDeviceCli::from_str("mem:1G,pcie_port=rp0").unwrap();
4013 assert_eq!(cfg.hdm_size, 1024 * 1024 * 1024);
4014 assert_eq!(cfg.pcie_port, "rp0");
4015 }
4016
4017 #[test]
4018 fn test_cxl_test_device_cli_parse_invalid() {
4019 assert!(CxlTestDeviceCli::from_str("file:disk.img,pcie_port=rp0").is_err());
4020 assert!(CxlTestDeviceCli::from_str("mem:1G").is_err());
4021 assert!(CxlTestDeviceCli::from_str("mem:1G,pcie_port=").is_err());
4022 }
4023
4024 #[test]
4025 fn test_fs_args_pcie_port() {
4026 let args = FsArgs::from_str("myfs,/path").unwrap();
4028 assert_eq!(args.tag, "myfs");
4029 assert_eq!(args.path, "/path");
4030 assert!(args.pcie_port.is_none());
4031
4032 let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
4034 assert_eq!(args.pcie_port.unwrap(), "rp0");
4035 assert_eq!(args.tag, "myfs");
4036 assert_eq!(args.path, "/path");
4037
4038 assert!(FsArgs::from_str("myfs").is_err());
4040 assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
4041 }
4042
4043 #[test]
4044 fn test_fs_args_with_options_pcie_port() {
4045 let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
4047 assert_eq!(args.tag, "myfs");
4048 assert_eq!(args.path, "/path");
4049 assert_eq!(args.options, "uid=1000");
4050 assert!(args.pcie_port.is_none());
4051
4052 let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
4054 assert_eq!(args.pcie_port.unwrap(), "rp0");
4055 assert_eq!(args.tag, "myfs");
4056 assert_eq!(args.path, "/path");
4057 assert_eq!(args.options, "uid=1000");
4058
4059 assert!(FsArgsWithOptions::from_str("myfs").is_err());
4061 }
4062
4063 #[test]
4064 fn test_virtio_pmem_args_pcie_port() {
4065 let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
4067 assert_eq!(args.path, "/path/to/file");
4068 assert!(args.pcie_port.is_none());
4069
4070 let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
4072 assert_eq!(args.pcie_port.unwrap(), "rp0");
4073 assert_eq!(args.path, "/path/to/file");
4074
4075 assert!(VirtioPmemArgs::from_str("").is_err());
4077 assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
4078 }
4079
4080 #[test]
4081 fn test_smt_config_from_str() {
4082 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
4083 assert_eq!(
4084 SmtConfigCli::from_str("force").unwrap(),
4085 SmtConfigCli::Force
4086 );
4087 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
4088
4089 assert!(SmtConfigCli::from_str("invalid").is_err());
4091 assert!(SmtConfigCli::from_str("").is_err());
4092 }
4093
4094 #[test]
4095 fn test_pcat_boot_order_from_str() {
4096 let order = PcatBootOrderCli::from_str("optical").unwrap();
4098 assert_eq!(order.0[0], PcatBootDevice::Optical);
4099
4100 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
4102 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
4103 assert_eq!(order.0[1], PcatBootDevice::Network);
4104
4105 assert!(PcatBootOrderCli::from_str("invalid").is_err());
4107 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
4109
4110 #[test]
4111 fn test_floppy_disk_from_str() {
4112 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
4114 assert!(!disk.read_only);
4115 match disk.kind {
4116 DiskCliKind::File {
4117 path,
4118 create_with_len,
4119 ..
4120 } => {
4121 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
4122 assert_eq!(create_with_len, None);
4123 }
4124 _ => panic!("Expected File variant"),
4125 }
4126
4127 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
4129 assert!(disk.read_only);
4130
4131 assert!(FloppyDiskCli::from_str("").is_err());
4133 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
4134 }
4135
4136 #[test]
4137 fn test_pcie_root_complex_from_str() {
4138 const ONE_MB: u64 = 1024 * 1024;
4139 const ONE_GB: u64 = 1024 * ONE_MB;
4140
4141 const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
4142 const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
4143 const DEFAULT_HDM: u64 = ONE_GB;
4144 const DEFAULT_HDM_WINDOW_RESTRICTIONS: CfmwsWindowRestrictions =
4145 CfmwsWindowRestrictions::DEVICE_COHERENT;
4146
4147 assert_eq!(
4148 PcieRootComplexCli::from_str("rc0").unwrap(),
4149 PcieRootComplexCli {
4150 name: "rc0".to_string(),
4151 segment: 0,
4152 start_bus: 0,
4153 end_bus: 255,
4154 low_mmio: DEFAULT_LOW_MMIO,
4155 high_mmio: DEFAULT_HIGH_MMIO,
4156 hdm: DEFAULT_HDM,
4157 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4158 vnode: None,
4159 low_mmio_base: None,
4160 high_mmio_base: None,
4161 preserve_bars: false,
4162 }
4163 );
4164
4165 assert_eq!(
4166 PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
4167 PcieRootComplexCli {
4168 name: "rc1".to_string(),
4169 segment: 1,
4170 start_bus: 0,
4171 end_bus: 255,
4172 low_mmio: DEFAULT_LOW_MMIO,
4173 high_mmio: DEFAULT_HIGH_MMIO,
4174 hdm: DEFAULT_HDM,
4175 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4176 vnode: None,
4177 low_mmio_base: None,
4178 high_mmio_base: None,
4179 preserve_bars: false,
4180 }
4181 );
4182
4183 assert_eq!(
4184 PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
4185 PcieRootComplexCli {
4186 name: "rc2".to_string(),
4187 segment: 0,
4188 start_bus: 32,
4189 end_bus: 255,
4190 low_mmio: DEFAULT_LOW_MMIO,
4191 high_mmio: DEFAULT_HIGH_MMIO,
4192 hdm: DEFAULT_HDM,
4193 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4194 vnode: None,
4195 low_mmio_base: None,
4196 high_mmio_base: None,
4197 preserve_bars: false,
4198 }
4199 );
4200
4201 assert_eq!(
4202 PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
4203 PcieRootComplexCli {
4204 name: "rc3".to_string(),
4205 segment: 0,
4206 start_bus: 0,
4207 end_bus: 31,
4208 low_mmio: DEFAULT_LOW_MMIO,
4209 high_mmio: DEFAULT_HIGH_MMIO,
4210 hdm: DEFAULT_HDM,
4211 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4212 vnode: None,
4213 low_mmio_base: None,
4214 high_mmio_base: None,
4215 preserve_bars: false,
4216 }
4217 );
4218
4219 assert_eq!(
4220 PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
4221 PcieRootComplexCli {
4222 name: "rc4".to_string(),
4223 segment: 0,
4224 start_bus: 32,
4225 end_bus: 127,
4226 low_mmio: DEFAULT_LOW_MMIO,
4227 high_mmio: 2 * ONE_GB,
4228 hdm: DEFAULT_HDM,
4229 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4230 vnode: None,
4231 low_mmio_base: None,
4232 high_mmio_base: None,
4233 preserve_bars: false,
4234 }
4235 );
4236
4237 assert_eq!(
4238 PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
4239 PcieRootComplexCli {
4240 name: "rc5".to_string(),
4241 segment: 2,
4242 start_bus: 32,
4243 end_bus: 127,
4244 low_mmio: DEFAULT_LOW_MMIO,
4245 high_mmio: DEFAULT_HIGH_MMIO,
4246 hdm: DEFAULT_HDM,
4247 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4248 vnode: None,
4249 low_mmio_base: None,
4250 high_mmio_base: None,
4251 preserve_bars: false,
4252 }
4253 );
4254
4255 assert_eq!(
4256 PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
4257 PcieRootComplexCli {
4258 name: "rc6".to_string(),
4259 segment: 0,
4260 start_bus: 0,
4261 end_bus: 255,
4262 low_mmio: ONE_MB as u32,
4263 high_mmio: 64 * ONE_GB,
4264 hdm: DEFAULT_HDM,
4265 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4266 vnode: None,
4267 low_mmio_base: None,
4268 high_mmio_base: None,
4269 preserve_bars: false,
4270 }
4271 );
4272
4273 assert_eq!(
4274 PcieRootComplexCli::from_str("rc7,hdm=2G").unwrap(),
4275 PcieRootComplexCli {
4276 name: "rc7".to_string(),
4277 segment: 0,
4278 start_bus: 0,
4279 end_bus: 255,
4280 low_mmio: DEFAULT_LOW_MMIO,
4281 high_mmio: DEFAULT_HIGH_MMIO,
4282 hdm: 2 * ONE_GB,
4283 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4284 vnode: None,
4285 low_mmio_base: None,
4286 high_mmio_base: None,
4287 preserve_bars: false,
4288 }
4289 );
4290
4291 assert_eq!(
4292 PcieRootComplexCli::from_str("rc8,hdm_window_restrictions=0x21").unwrap(),
4293 PcieRootComplexCli {
4294 name: "rc8".to_string(),
4295 segment: 0,
4296 start_bus: 0,
4297 end_bus: 255,
4298 low_mmio: DEFAULT_LOW_MMIO,
4299 high_mmio: DEFAULT_HIGH_MMIO,
4300 hdm: DEFAULT_HDM,
4301 hdm_window_restrictions: CfmwsWindowRestrictions::try_from_bits(0x21).unwrap(),
4302 vnode: None,
4303 low_mmio_base: None,
4304 high_mmio_base: None,
4305 preserve_bars: false,
4306 }
4307 );
4308
4309 assert!(PcieRootComplexCli::from_str("").is_err());
4311 assert!(PcieRootComplexCli::from_str("poorly,").is_err());
4312 assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
4313 assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
4314 assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
4315 assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
4316 assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
4317 assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
4318 assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
4319 assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
4320 assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
4321 assert!(PcieRootComplexCli::from_str("rc,hdm=bad").is_err());
4322 assert!(PcieRootComplexCli::from_str("rc,hdm").is_err());
4323 assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions=bad").is_err());
4324 assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions").is_err());
4325 assert!(PcieRootComplexCli::from_str("rc,cxl").is_err());
4326
4327 assert_eq!(
4329 PcieRootComplexCli::from_str("rc9,node=1").unwrap(),
4330 PcieRootComplexCli {
4331 name: "rc9".to_string(),
4332 segment: 0,
4333 start_bus: 0,
4334 end_bus: 255,
4335 low_mmio: DEFAULT_LOW_MMIO,
4336 high_mmio: DEFAULT_HIGH_MMIO,
4337 hdm: DEFAULT_HDM,
4338 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4339 vnode: Some(1),
4340 low_mmio_base: None,
4341 high_mmio_base: None,
4342 preserve_bars: false,
4343 }
4344 );
4345 }
4346
4347 #[test]
4348 fn test_pcie_root_port_from_str() {
4349 assert_eq!(
4350 PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
4351 PcieRootPortCli {
4352 root_complex_name: "rc0".to_string(),
4353 name: "rc0rp0".to_string(),
4354 devfn: None,
4355 hotplug: false,
4356 acs_capabilities_supported: None,
4357 cxl: false,
4358 pasid: false,
4359 }
4360 );
4361
4362 assert_eq!(
4363 PcieRootPortCli::from_str("my_rc:port2").unwrap(),
4364 PcieRootPortCli {
4365 root_complex_name: "my_rc".to_string(),
4366 name: "port2".to_string(),
4367 devfn: None,
4368 hotplug: false,
4369 acs_capabilities_supported: None,
4370 cxl: false,
4371 pasid: false,
4372 }
4373 );
4374
4375 assert_eq!(
4377 PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
4378 PcieRootPortCli {
4379 root_complex_name: "my_rc".to_string(),
4380 name: "port2".to_string(),
4381 devfn: None,
4382 hotplug: true,
4383 acs_capabilities_supported: None,
4384 cxl: false,
4385 pasid: false,
4386 }
4387 );
4388
4389 assert_eq!(
4390 PcieRootPortCli::from_str("my_rc:port3,acs=0").unwrap(),
4391 PcieRootPortCli {
4392 root_complex_name: "my_rc".to_string(),
4393 name: "port3".to_string(),
4394 devfn: None,
4395 hotplug: false,
4396 acs_capabilities_supported: Some(0),
4397 cxl: false,
4398 pasid: false,
4399 }
4400 );
4401
4402 assert_eq!(
4403 PcieRootPortCli::from_str("my_rc:port3,acs=0x5f").unwrap(),
4404 PcieRootPortCli {
4405 root_complex_name: "my_rc".to_string(),
4406 name: "port3".to_string(),
4407 devfn: None,
4408 hotplug: false,
4409 acs_capabilities_supported: Some(0x005f),
4410 cxl: false,
4411 pasid: false,
4412 }
4413 );
4414
4415 assert_eq!(
4416 PcieRootPortCli::from_str("my_rc:port4,cxl").unwrap(),
4417 PcieRootPortCli {
4418 root_complex_name: "my_rc".to_string(),
4419 name: "port4".to_string(),
4420 devfn: None,
4421 hotplug: false,
4422 acs_capabilities_supported: None,
4423 cxl: true,
4424 pasid: false,
4425 }
4426 );
4427
4428 assert_eq!(
4430 PcieRootPortCli::from_str("my_rc:port5,addr=5").unwrap(),
4431 PcieRootPortCli {
4432 root_complex_name: "my_rc".to_string(),
4433 name: "port5".to_string(),
4434 devfn: Some(5 << 3),
4435 hotplug: false,
4436 acs_capabilities_supported: None,
4437 cxl: false,
4438 pasid: false,
4439 }
4440 );
4441 assert_eq!(
4442 PcieRootPortCli::from_str("my_rc:port6,addr=5.1").unwrap(),
4443 PcieRootPortCli {
4444 root_complex_name: "my_rc".to_string(),
4445 name: "port6".to_string(),
4446 devfn: Some((5 << 3) | 1),
4447 hotplug: false,
4448 acs_capabilities_supported: None,
4449 cxl: false,
4450 pasid: false,
4451 }
4452 );
4453 assert_eq!(
4454 PcieRootPortCli::from_str("my_rc:port7,addr=0x1f.7").unwrap(),
4455 PcieRootPortCli {
4456 root_complex_name: "my_rc".to_string(),
4457 name: "port7".to_string(),
4458 devfn: Some(0xff),
4459 hotplug: false,
4460 acs_capabilities_supported: None,
4461 cxl: false,
4462 pasid: false,
4463 }
4464 );
4465
4466 assert_eq!(
4467 PcieRootPortCli::from_str("my_rc:port8,pasid").unwrap(),
4468 PcieRootPortCli {
4469 root_complex_name: "my_rc".to_string(),
4470 name: "port8".to_string(),
4471 devfn: None,
4472 hotplug: false,
4473 acs_capabilities_supported: None,
4474 cxl: false,
4475 pasid: true,
4476 }
4477 );
4478
4479 assert!(PcieRootPortCli::from_str("").is_err());
4481 assert!(PcieRootPortCli::from_str("rp0").is_err());
4482 assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
4483 assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
4484 assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
4485 assert!(PcieRootPortCli::from_str("rc0:rp0,cxl=true").is_err());
4486 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=32").is_err());
4487 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=0.8").is_err());
4488 assert!(PcieRootPortCli::from_str("rc0:rp0,addr=1.2.3").is_err());
4489 assert!(PcieRootPortCli::from_str("rc0:rp0,addr").is_err());
4490 assert!(PcieRootPortCli::from_str("rc0:rp0,pasid=foo").is_err());
4491 }
4492
4493 #[test]
4494 fn test_pcie_generic_initiator_from_str() {
4495 assert_eq!(
4496 PcieGenericInitiatorCli::from_str("port=rp0,node=1").unwrap(),
4497 PcieGenericInitiatorCli {
4498 port_name: "rp0".to_string(),
4499 node: 1,
4500 }
4501 );
4502
4503 assert_eq!(
4505 PcieGenericInitiatorCli::from_str("node=2,port=sw0-downstream-1").unwrap(),
4506 PcieGenericInitiatorCli {
4507 port_name: "sw0-downstream-1".to_string(),
4508 node: 2,
4509 }
4510 );
4511
4512 assert!(PcieGenericInitiatorCli::from_str("").is_err());
4514 assert!(PcieGenericInitiatorCli::from_str("port=rp0").is_err());
4515 assert!(PcieGenericInitiatorCli::from_str("node=1").is_err());
4516 assert!(PcieGenericInitiatorCli::from_str("rp0=1").is_err());
4517 assert!(PcieGenericInitiatorCli::from_str("port=,node=1").is_err());
4518 assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=x").is_err());
4519 assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=1,extra").is_err());
4520 }
4521
4522 #[test]
4523 fn test_pcie_switch_from_str() {
4524 assert_eq!(
4525 GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
4526 GenericPcieSwitchCli {
4527 port_name: "rp0".to_string(),
4528 name: "switch0".to_string(),
4529 num_downstream_ports: 4,
4530 hotplug: false,
4531 acs_capabilities_supported: None,
4532 pasid: false,
4533 }
4534 );
4535
4536 assert_eq!(
4537 GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
4538 GenericPcieSwitchCli {
4539 port_name: "port1".to_string(),
4540 name: "my_switch".to_string(),
4541 num_downstream_ports: 4,
4542 hotplug: false,
4543 acs_capabilities_supported: None,
4544 pasid: false,
4545 }
4546 );
4547
4548 assert_eq!(
4549 GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
4550 GenericPcieSwitchCli {
4551 port_name: "rp2".to_string(),
4552 name: "sw".to_string(),
4553 num_downstream_ports: 8,
4554 hotplug: false,
4555 acs_capabilities_supported: None,
4556 pasid: false,
4557 }
4558 );
4559
4560 assert_eq!(
4562 GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
4563 GenericPcieSwitchCli {
4564 port_name: "switch0-downstream-1".to_string(),
4565 name: "child_switch".to_string(),
4566 num_downstream_ports: 4,
4567 hotplug: false,
4568 acs_capabilities_supported: None,
4569 pasid: false,
4570 }
4571 );
4572
4573 assert_eq!(
4575 GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
4576 GenericPcieSwitchCli {
4577 port_name: "rp0".to_string(),
4578 name: "switch0".to_string(),
4579 num_downstream_ports: 4,
4580 hotplug: true,
4581 acs_capabilities_supported: None,
4582 pasid: false,
4583 }
4584 );
4585
4586 assert_eq!(
4588 GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
4589 GenericPcieSwitchCli {
4590 port_name: "rp0".to_string(),
4591 name: "switch0".to_string(),
4592 num_downstream_ports: 8,
4593 hotplug: true,
4594 acs_capabilities_supported: None,
4595 pasid: false,
4596 }
4597 );
4598
4599 assert_eq!(
4600 GenericPcieSwitchCli::from_str("rp0:switch0,acs=0").unwrap(),
4601 GenericPcieSwitchCli {
4602 port_name: "rp0".to_string(),
4603 name: "switch0".to_string(),
4604 num_downstream_ports: 4,
4605 hotplug: false,
4606 acs_capabilities_supported: Some(0),
4607 pasid: false,
4608 }
4609 );
4610
4611 assert_eq!(
4612 GenericPcieSwitchCli::from_str("rp0:switch0,acs=95").unwrap(),
4613 GenericPcieSwitchCli {
4614 port_name: "rp0".to_string(),
4615 name: "switch0".to_string(),
4616 num_downstream_ports: 4,
4617 hotplug: false,
4618 acs_capabilities_supported: Some(95),
4619 pasid: false,
4620 }
4621 );
4622
4623 assert_eq!(
4624 GenericPcieSwitchCli::from_str("rp0:switch0,pasid").unwrap(),
4625 GenericPcieSwitchCli {
4626 port_name: "rp0".to_string(),
4627 name: "switch0".to_string(),
4628 num_downstream_ports: 4,
4629 hotplug: false,
4630 acs_capabilities_supported: None,
4631 pasid: true,
4632 }
4633 );
4634
4635 assert!(GenericPcieSwitchCli::from_str("").is_err());
4637 assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
4638 assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
4639 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
4640 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
4641 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
4642 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
4643 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,pasid=bar").is_err());
4644 }
4645
4646 #[test]
4647 fn test_pcie_remote_from_str() {
4648 assert_eq!(
4650 PcieRemoteCli::from_str("rc0rp0").unwrap(),
4651 PcieRemoteCli {
4652 port_name: "rc0rp0".to_string(),
4653 socket_addr: None,
4654 hu: 0,
4655 controller: 0,
4656 }
4657 );
4658
4659 assert_eq!(
4661 PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
4662 PcieRemoteCli {
4663 port_name: "rc0rp0".to_string(),
4664 socket_addr: Some("localhost:22567".to_string()),
4665 hu: 0,
4666 controller: 0,
4667 }
4668 );
4669
4670 assert_eq!(
4672 PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
4673 PcieRemoteCli {
4674 port_name: "myport".to_string(),
4675 socket_addr: Some("localhost:22568".to_string()),
4676 hu: 1,
4677 controller: 2,
4678 }
4679 );
4680
4681 assert_eq!(
4683 PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
4684 PcieRemoteCli {
4685 port_name: "port0".to_string(),
4686 socket_addr: None,
4687 hu: 5,
4688 controller: 3,
4689 }
4690 );
4691
4692 assert!(PcieRemoteCli::from_str("").is_err());
4694 assert!(PcieRemoteCli::from_str("port,socket=").is_err());
4695 assert!(PcieRemoteCli::from_str("port,hu=").is_err());
4696 assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
4697 assert!(PcieRemoteCli::from_str("port,controller=").is_err());
4698 assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
4699 assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
4700 }
4701
4702 #[test]
4703 fn test_parse_memory_units() {
4704 assert_eq!(parse_memory("64G").unwrap(), 64 * 1024 * 1024 * 1024);
4705 assert_eq!(parse_memory("64GB").unwrap(), 64 * 1024 * 1024 * 1024);
4706 assert_eq!(parse_memory("3MB").unwrap(), 3 * 1024 * 1024);
4707 assert_eq!(parse_memory("512KB").unwrap(), 512 * 1024);
4708 assert!(parse_memory("3MiB").is_err());
4709 }
4710
4711 #[test]
4712 fn test_memory_config_size_only() {
4713 assert_eq!(
4714 parse_memory_config("64G").unwrap(),
4715 MemoryCli {
4716 size: Some(vmm_cli::MemorySize(64 * 1024 * 1024 * 1024)),
4717 ..Default::default()
4718 }
4719 );
4720 }
4721
4722 #[test]
4723 fn test_memory_config_key_value() {
4724 assert_eq!(
4725 parse_memory_config("size=2G,shared=off,prefetch=on,thp=on").unwrap(),
4726 MemoryCli {
4727 size: Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024)),
4728 shared: Some(false),
4729 prefetch: true,
4730 transparent_hugepages: Some(true),
4731 ..Default::default()
4732 }
4733 );
4734
4735 assert_eq!(
4736 parse_memory_config("size=4GB,hugepages=on,hugepage_size=2MB").unwrap(),
4737 MemoryCli {
4738 size: Some(vmm_cli::MemorySize(4 * 1024 * 1024 * 1024)),
4739 hugepages: true,
4740 hugepage_size: Some(vmm_cli::MemorySize(2 * 1024 * 1024)),
4741 ..Default::default()
4742 }
4743 );
4744
4745 assert_eq!(
4746 parse_memory_config("file=/tmp/memory.bin").unwrap(),
4747 MemoryCli {
4748 file: Some(PathBuf::from("/tmp/memory.bin")),
4749 ..Default::default()
4750 }
4751 );
4752 }
4753
4754 #[test]
4755 fn test_memory_config_rejects_invalid_combinations() {
4756 assert!(parse_memory_config("size=1G,size=2G").is_err());
4757 assert!(parse_memory_config("hugepage_size=2M").is_err());
4758 assert!(parse_memory_config("hugepages=on,shared=off").is_err());
4759 assert!(parse_memory_config("hugepages=on,file=/tmp/memory.bin").is_err());
4760
4761 assert_eq!(
4764 parse_memory_config("hugepages=on,hugepage_size=3MB")
4765 .unwrap()
4766 .hugepage_size,
4767 Some(vmm_cli::MemorySize(3 * 1024 * 1024))
4768 );
4769 }
4770
4771 #[test]
4772 fn test_memory_options_merge_legacy_aliases() {
4773 let opt = Options::try_parse_from([
4774 "openvmm",
4775 "--memory",
4776 "2G",
4777 "--prefetch",
4778 "--private-memory",
4779 "--thp",
4780 ])
4781 .unwrap();
4782 opt.validate_memory_options().unwrap();
4783 assert_eq!(opt.memory_size(), 2 * 1024 * 1024 * 1024);
4784 assert!(opt.prefetch_memory());
4785 assert!(opt.private_memory());
4786 assert!(opt.transparent_hugepages());
4787 }
4788
4789 #[test]
4790 fn test_serial_debugger_mode_option_parsed() {
4791 let opt = Options::try_parse_from(["openvmm"]).unwrap();
4793 assert!(opt.com1.is_none());
4794
4795 let opt = Options::try_parse_from(["openvmm", "--com1", "none"]).unwrap();
4797 let com1 = opt.com1.unwrap();
4798 assert!(!com1.debugger_mode);
4799 assert_eq!(com1.backend, SerialConfigCli::None);
4800
4801 let opt = Options::try_parse_from([
4804 "openvmm",
4805 "--com1",
4806 "debugger-mode:listen=/tmp/kd",
4807 "--com2",
4808 "none",
4809 ])
4810 .unwrap();
4811 let com1 = opt.com1.unwrap();
4812 assert!(com1.debugger_mode);
4813 assert_eq!(com1.backend, SerialConfigCli::Pipe("/tmp/kd".into()));
4814 assert!(!opt.com2.unwrap().debugger_mode);
4816
4817 let opt = Options::try_parse_from([
4819 "openvmm",
4820 "--com1",
4821 "debugger-mode:listen=tcp:127.0.0.1:5555",
4822 ])
4823 .unwrap();
4824 let com1 = opt.com1.unwrap();
4825 assert!(com1.debugger_mode);
4826 assert_eq!(
4827 com1.backend,
4828 SerialConfigCli::Tcp("127.0.0.1:5555".parse().unwrap())
4829 );
4830 }
4831
4832 #[test]
4833 fn test_memory_options_allow_legacy_thp_with_new_private_memory() {
4834 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=off", "--thp"]).unwrap();
4835 opt.validate_memory_options().unwrap();
4836 assert!(opt.private_memory());
4837 assert!(opt.transparent_hugepages());
4838 }
4839
4840 #[test]
4841 fn test_memory_options_reject_conflicting_legacy_aliases() {
4842 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=on", "--private-memory"])
4843 .unwrap();
4844 assert!(opt.validate_memory_options().is_err());
4845 }
4846
4847 #[test]
4848 fn test_pidfile_option_parsed() {
4849 let opt = Options::try_parse_from(["openvmm", "--pidfile", "/tmp/test.pid"]).unwrap();
4850 assert_eq!(opt.pidfile, Some(PathBuf::from("/tmp/test.pid")));
4851 }
4852
4853 #[test]
4854 fn test_guest_power_action_flags() {
4855 let opt = Options::try_parse_from(["openvmm"]).unwrap();
4858 assert_eq!(opt.guest_reset_action, GuestPowerAction::Reset);
4859 assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Halt);
4860 assert_eq!(opt.guest_crash_action, GuestPowerAction::Halt);
4861 assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Reset);
4862 assert_eq!(
4865 crate::vm_controller::GuestPowerActions {
4866 shutdown: opt.guest_shutdown_action,
4867 reset: opt.guest_reset_action,
4868 crash: opt.guest_crash_action,
4869 watchdog: opt.guest_watchdog_action,
4870 },
4871 crate::vm_controller::GuestPowerActions::default(),
4872 );
4873
4874 let opt = Options::try_parse_from([
4875 "openvmm",
4876 "--guest-watchdog",
4877 "--guest-reset-action",
4878 "exit",
4879 "--guest-shutdown-action",
4880 "exit:5",
4881 "--guest-crash-action",
4882 "reset",
4883 "--guest-watchdog-action",
4884 "halt",
4885 ])
4886 .unwrap();
4887 assert_eq!(opt.guest_reset_action, GuestPowerAction::Exit(0));
4889 assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Exit(5));
4890 assert_eq!(opt.guest_crash_action, GuestPowerAction::Reset);
4891 assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Halt);
4892
4893 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:nope"]).is_err());
4895 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:300"]).is_err());
4896 assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:-1"]).is_err());
4897
4898 assert!(Options::try_parse_from(["openvmm", "--guest-watchdog-action", "halt"]).is_err());
4900 }
4901
4902 #[cfg(target_os = "linux")]
4903 #[test]
4904 fn test_vfio_device_cli_parse() {
4905 use vfio_assigned_device_resources::BarAddressConfig;
4906
4907 let v = VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0").unwrap();
4909 assert_eq!(v.pci_id, "0000:01:00.0");
4910 assert_eq!(v.port_name, "rp0");
4911 assert_eq!(v.iommu, None);
4912
4913 let v = VfioDeviceCli::from_str("port=rp1,iommu=iommu0,host=0000:02:00.0").unwrap();
4915 assert_eq!(v.pci_id, "0000:02:00.0");
4916 assert_eq!(v.port_name, "rp1");
4917 assert_eq!(v.iommu.as_deref(), Some("iommu0"));
4918
4919 let v = VfioDeviceCli::from_str(
4920 "host=0000:03:00.0,port=rp2,bar0=host,bar2=0x80000000,bar4=0x110000000000",
4921 )
4922 .unwrap();
4923 assert_eq!(v.bar_addresses[0], BarAddressConfig::HostAssigned);
4924 assert_eq!(v.bar_addresses[1], BarAddressConfig::GuestAssigned);
4925 assert_eq!(v.bar_addresses[2], BarAddressConfig::Fixed(0x80000000));
4926 assert_eq!(v.bar_addresses[4], BarAddressConfig::Fixed(0x110000000000));
4927 }
4928
4929 #[cfg(target_os = "linux")]
4930 #[test]
4931 fn test_vfio_device_cli_errors() {
4932 assert!(VfioDeviceCli::from_str("port=rp0").is_err());
4934 assert!(VfioDeviceCli::from_str("host=0000:01:00.0").is_err());
4935
4936 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,foo=bar").is_err());
4938
4939 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,host=0000:02:00.0,port=rp0").is_err());
4941 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,port=rp1").is_err());
4942 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=a,iommu=b").is_err());
4943
4944 assert!(VfioDeviceCli::from_str("host=,port=rp0").is_err());
4946 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=").is_err());
4947 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=").is_err());
4948
4949 assert!(VfioDeviceCli::from_str("host").is_err());
4951 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu").is_err());
4952
4953 assert!(VfioDeviceCli::from_str("host=../../etc/passwd,port=rp0").is_err());
4955 assert!(VfioDeviceCli::from_str("host=foo/bar,port=rp0").is_err());
4956
4957 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0").is_err());
4959 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x0").is_err());
4960 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0xnope").is_err());
4961 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=pt").is_err());
4962 assert!(
4963 VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x1000,bar0=host").is_err()
4964 );
4965 }
4966
4967 #[cfg(target_os = "linux")]
4968 #[test]
4969 fn test_iommu_cli_parse() {
4970 let c = IommuCli::from_str("id=iommu0").unwrap();
4971 assert_eq!(c.id, "iommu0");
4972
4973 assert!(IommuCli::from_str("name=iommu0").is_err());
4975
4976 assert!(IommuCli::from_str("iommu0").is_err());
4978
4979 assert!(IommuCli::from_str("id=").is_err());
4981 }
4982
4983 #[cfg(target_os = "linux")]
4984 #[test]
4985 fn test_vhost_user_cli() {
4986 let v = VhostUserCli::from_str("/run/blk.sock,type=blk,num_queues=2").unwrap();
4988 assert_eq!(v.socket_path, "/run/blk.sock");
4989 assert!(matches!(
4990 v.device_type,
4991 VhostUserDeviceTypeCli::Blk {
4992 num_queues: Some(2),
4993 queue_size: None
4994 }
4995 ));
4996 assert_eq!(v.pcie_port, None);
4997
4998 let v = VhostUserCli::from_str("/run/fs.sock,type=fs,tag=myfs,pcie_port=p0").unwrap();
5000 assert!(matches!(
5001 &v.device_type,
5002 VhostUserDeviceTypeCli::Fs { tag, .. } if tag == "myfs"
5003 ));
5004 assert_eq!(v.pcie_port.as_deref(), Some("p0"));
5005
5006 let v = VhostUserCli::from_str("/run/x.sock,device_id=9,queue_sizes=[16,32]").unwrap();
5008 assert!(matches!(
5009 &v.device_type,
5010 VhostUserDeviceTypeCli::Other { device_id: 9, queue_sizes } if *queue_sizes == vec![16, 32]
5011 ));
5012
5013 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()); }
5024
5025 #[cfg(target_os = "linux")]
5026 #[test]
5027 fn test_vhost_vsock_cli() {
5028 let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "3"]).unwrap();
5029 assert_eq!(opt.virtio_vsock_vhost_cid, Some(3));
5030
5031 assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "2"]).is_err());
5032 assert!(
5033 Options::try_parse_from([
5034 "openvmm",
5035 "--virtio-vsock-vhost-cid",
5036 "3",
5037 "--virtio-vsock-path",
5038 "/tmp/vsock",
5039 ])
5040 .is_err()
5041 );
5042 }
5043
5044 #[test]
5045 fn test_nvme_controller_cli_pcie() {
5046 let c = NvmeControllerCli::from_str("id=nvme0,pcie_port=p0").unwrap();
5047 assert_eq!(c.id, "nvme0");
5048 assert_eq!(c.transport, NvmeControllerTransport::Pcie("p0".into()));
5049 }
5050
5051 #[test]
5052 fn test_nvme_controller_cli_vpci_no_guid() {
5053 let c = NvmeControllerCli::from_str("id=nvme1,vpci").unwrap();
5054 assert_eq!(c.id, "nvme1");
5055 assert!(matches!(c.transport, NvmeControllerTransport::Vpci(None)));
5056 assert!(NvmeControllerCli::from_str("id=nvme1,vpci=").is_err());
5057 }
5058
5059 #[test]
5060 fn test_nvme_controller_cli_vpci_with_guid() {
5061 let c = NvmeControllerCli::from_str("id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c")
5062 .unwrap();
5063 assert_eq!(c.id, "nvme2");
5064 assert!(matches!(
5065 c.transport,
5066 NvmeControllerTransport::Vpci(Some(_))
5067 ));
5068 }
5069
5070 #[test]
5071 fn test_nvme_controller_cli_errors() {
5072 assert!(NvmeControllerCli::from_str("pcie_port=p0").is_err());
5074 assert!(NvmeControllerCli::from_str("id=nvme0").is_err());
5076 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,vpci").is_err());
5078 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,foo=bar").is_err());
5080 assert!(NvmeControllerCli::from_str("id=,pcie_port=p0").is_err());
5082 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=").is_err());
5084 let err = NvmeControllerCli::from_str("id=nvme0,vpci=not-a-guid").unwrap_err();
5086 assert!(err.to_string().contains("invalid value for option 'vpci'"));
5087 }
5088
5089 #[test]
5090 fn test_disk_cli_controller() {
5091 let d = DiskCli::from_str("file:disk.vhd,on=nvme0").unwrap();
5092 assert_eq!(d.controller.as_deref(), Some("nvme0"));
5093 assert_eq!(d.nsid, None);
5094 }
5095
5096 #[test]
5097 fn test_disk_cli_controller_with_nsid() {
5098 let d = DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=3").unwrap();
5099 assert_eq!(d.controller.as_deref(), Some("nvme0"));
5100 assert_eq!(d.nsid, Some(3));
5101 }
5102
5103 #[test]
5104 fn test_disk_cli_controller_errors() {
5105 assert!(DiskCli::from_str("file:disk.vhd,nsid=1").is_err());
5107 assert!(DiskCli::from_str("file:disk.vhd,lun=0").is_err());
5109 assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,pcie_port=p0").is_err());
5111 assert!(DiskCli::from_str("file:disk.vhd,on=").is_err());
5113 assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=abc").is_err());
5115 assert!(DiskCli::from_str("file:disk.vhd,on=c,nsid=1,lun=0").is_err());
5117 }
5118
5119 #[test]
5120 fn test_disk_cli_controller_with_lun() {
5121 let d = DiskCli::from_str("file:disk.vhd,on=scsi0,lun=3").unwrap();
5122 assert_eq!(d.controller.as_deref(), Some("scsi0"));
5123 assert_eq!(d.lun, Some(3));
5124 assert_eq!(d.nsid, None);
5125 }
5126
5127 #[test]
5128 fn test_scsi_controller_cli() {
5129 let c = ScsiControllerCli::from_str("id=scsi0").unwrap();
5130 assert_eq!(c.id, "scsi0");
5131 assert_eq!(c.sub_channels, 0);
5132 }
5133
5134 #[test]
5135 fn test_scsi_controller_cli_with_sub_channels() {
5136 let c = ScsiControllerCli::from_str("id=scsi1,sub_channels=4").unwrap();
5137 assert_eq!(c.id, "scsi1");
5138 assert_eq!(c.sub_channels, 4);
5139 }
5140
5141 #[test]
5142 fn test_scsi_controller_cli_errors() {
5143 assert!(ScsiControllerCli::from_str("sub_channels=4").is_err());
5145 assert!(ScsiControllerCli::from_str("id=").is_err());
5147 assert!(ScsiControllerCli::from_str("id=scsi0,foo=bar").is_err());
5149 assert!(ScsiControllerCli::from_str("id=scsi0,sub_channels=abc").is_err());
5151 }
5152
5153 #[test]
5154 fn test_disk_cli_relay() {
5155 let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt").unwrap();
5156 assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
5157 assert_eq!(d.relay.as_ref().unwrap().1, None);
5158 }
5159
5160 #[test]
5161 fn test_disk_cli_relay_with_location() {
5162 let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:3").unwrap();
5163 assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
5164 assert_eq!(d.relay.as_ref().unwrap().1, Some(3));
5165 }
5166
5167 #[test]
5168 fn test_disk_cli_relay_errors() {
5169 assert!(DiskCli::from_str("file:disk.vhd,relay=tgt").is_err());
5171 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt,uh").is_err());
5173 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:abc").is_err());
5175 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=").is_err());
5177 }
5178
5179 #[test]
5180 fn test_nvme_controller_cli_vtl2() {
5181 let c = NvmeControllerCli::from_str("id=nvme0,vpci,vtl2").unwrap();
5182 assert_eq!(c.vtl, DeviceVtl::Vtl2);
5183 }
5184
5185 #[test]
5186 fn test_scsi_controller_cli_vtl2() {
5187 let c = ScsiControllerCli::from_str("id=scsi0,vtl2").unwrap();
5188 assert_eq!(c.vtl, DeviceVtl::Vtl2);
5189 }
5190
5191 #[test]
5192 fn test_openhcl_controller_cli() {
5193 let c = OpenhclControllerCli::from_str("id=vtl0-scsi,type=scsi").unwrap();
5194 assert_eq!(c.id, "vtl0-scsi");
5195 assert_eq!(c.controller_type, OpenhclControllerType::Scsi);
5196 assert_eq!(c.guid, None);
5197 }
5198
5199 #[test]
5200 fn test_openhcl_controller_cli_nvme_with_guid() {
5201 let c = OpenhclControllerCli::from_str(
5202 "id=vtl0-nvme,type=nvme,guid=09a59b81-2bf6-4164-81d7-3a0dc977ba65",
5203 )
5204 .unwrap();
5205 assert_eq!(c.controller_type, OpenhclControllerType::Nvme);
5206 assert!(c.guid.is_some());
5207 }
5208
5209 #[test]
5210 fn test_openhcl_controller_cli_errors() {
5211 assert!(OpenhclControllerCli::from_str("type=scsi").is_err());
5213 assert!(OpenhclControllerCli::from_str("id=foo").is_err());
5215 assert!(OpenhclControllerCli::from_str("id=foo,type=ide").is_err());
5217 assert!(OpenhclControllerCli::from_str("id=foo,type=scsi,guid=bad").is_err());
5219 }
5220
5221 #[test]
5222 fn test_parse_vp_list() {
5223 use vmm_cli::BracketRangeList;
5224
5225 let parse = |s: &str| {
5226 s.parse::<BracketRangeList>()
5227 .and_then(|v| v.expand_below(1024))
5228 };
5229
5230 assert_eq!(parse("[0,1,2,3]").unwrap(), vec![0, 1, 2, 3]);
5232
5233 assert_eq!(parse("[5]").unwrap(), vec![5]);
5235
5236 assert_eq!(parse("[0-3]").unwrap(), vec![0, 1, 2, 3]);
5238
5239 assert_eq!(parse("[0,1,4-6,10]").unwrap(), vec![0, 1, 4, 5, 6, 10]);
5241
5242 assert_eq!(parse("[0, 1, 2-4]").unwrap(), vec![0, 1, 2, 3, 4]);
5244
5245 assert!(parse("0,1,2").is_err());
5247 assert!(parse("0-3").is_err());
5248
5249 assert!(parse("[3-0]").is_err());
5251
5252 assert!(parse("[a,b]").is_err());
5254 }
5255
5256 #[test]
5257 fn test_parse_numa_node() {
5258 use super::parse_numa_node;
5259
5260 let n = parse_numa_node("size=2G").unwrap();
5262 assert_eq!(
5263 n.memory.size,
5264 Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024))
5265 );
5266 assert!(n.vps.is_none());
5267 assert!(n.host_numa_node.is_none());
5268
5269 let n = parse_numa_node("size=1G,vps=[0,1,2,3]").unwrap();
5271 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
5272
5273 let n = parse_numa_node("size=1G,vps=[0-3]").unwrap();
5275 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
5276
5277 let n = parse_numa_node("size=1G,host_numa_node=1").unwrap();
5279 assert_eq!(n.host_numa_node, Some(1));
5280
5281 let n = parse_numa_node("size=1G,vps=[0,1],host_numa_node=0,hugepages=on").unwrap();
5283 assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1]);
5284 assert_eq!(n.host_numa_node, Some(0));
5285 assert!(n.memory.hugepages);
5286
5287 assert!(parse_numa_node("vps=[0,1]").is_err());
5289
5290 assert!(parse_numa_node("size=1G,vps=0,1").is_err());
5292
5293 assert!(parse_numa_node("size=1G,vps=[0],vps=[1]").is_err());
5295
5296 assert!(parse_numa_node("size=1G,file=/tmp/x").is_err());
5298
5299 let n = parse_numa_node("size=1G,vps=[]").unwrap();
5301 assert!(n.vps.unwrap().0.is_empty());
5302 }
5303
5304 #[test]
5305 fn test_parse_numa_distance() {
5306 use super::parse_numa_distance;
5307
5308 let d = parse_numa_distance("0:1:20").unwrap();
5309 assert_eq!(d.src, 0);
5310 assert_eq!(d.dst, 1);
5311 assert_eq!(d.distance, 20);
5312
5313 let d = parse_numa_distance("0:0:10").unwrap();
5315 assert_eq!(d.distance, 10);
5316
5317 assert!(parse_numa_distance("0:1:5").is_err());
5319
5320 assert!(parse_numa_distance("0:1").is_err());
5322 assert!(parse_numa_distance("0:1:20:extra").is_err());
5323 }
5324
5325 #[cfg(guest_arch = "aarch64")]
5326 #[test]
5327 fn test_smmu_cli_from_str() {
5328 let s = SmmuCli::from_str("rc=pcie0").unwrap();
5330 assert_eq!(s.rc_name, "pcie0");
5331 assert!(!s.accel);
5332 assert!(matches!(s.oas, SmmuOasCli::Auto));
5333
5334 let s = SmmuCli::from_str("rc=pcie0,accel").unwrap();
5336 assert_eq!(s.rc_name, "pcie0");
5337 assert!(s.accel);
5338 assert!(matches!(s.oas, SmmuOasCli::Auto));
5339
5340 let s = SmmuCli::from_str("rc=pcie0,oas=auto").unwrap();
5342 assert!(matches!(s.oas, SmmuOasCli::Auto));
5343
5344 let s = SmmuCli::from_str("rc=pcie0,oas=52").unwrap();
5346 assert!(matches!(s.oas, SmmuOasCli::Fixed(52)));
5347
5348 let s = SmmuCli::from_str("oas=48,accel,rc=pcie1").unwrap();
5350 assert_eq!(s.rc_name, "pcie1");
5351 assert!(s.accel);
5352 assert!(matches!(s.oas, SmmuOasCli::Fixed(48)));
5353
5354 assert!(SmmuCli::from_str("accel").is_err());
5356 assert!(SmmuCli::from_str("oas=52").is_err());
5357
5358 assert!(SmmuCli::from_str("rc=").is_err());
5360
5361 assert!(SmmuCli::from_str("rc=pcie0,rc=pcie1").is_err());
5363
5364 assert!(SmmuCli::from_str("rc=pcie0,oas=big").is_err());
5366
5367 assert!(SmmuCli::from_str("rc=pcie0,foo=bar").is_err());
5369
5370 assert!(SmmuCli::from_str("rc=pcie0,turbo").is_err());
5372 }
5373}