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