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].
177
178Examples:
179 --numa size=2G --numa size=2G
180 --numa size=2G,host_numa_node=0 --numa size=2G,host_numa_node=1
181 --numa size=2G,hugepages=on,vps=[0,1] --numa size=2G,vps=[2,3]
182 --numa size=2G,vps=[0-3] --numa size=2G,vps=[4-7]"#
183 )]
184 pub numa: Option<Vec<NumaNodeCli>>,
185
186 #[clap(long, value_name = "SRC:DST:DIST", value_parser = parse_numa_distance, conflicts_with = "memory", requires = "numa")]
191 pub numa_distance: Option<Vec<NumaDistanceCli>>,
192
193 #[clap(short = 'M', long, hide = true)]
195 pub shared_memory: bool,
196
197 #[clap(long = "prefetch", hide = true, conflicts_with = "numa")]
199 pub deprecated_prefetch: bool,
200
201 #[clap(
205 long = "memory-backing-file",
206 value_name = "FILE",
207 hide = true,
208 conflicts_with_all = ["deprecated_private_memory", "numa"]
209 )]
210 pub deprecated_memory_backing_file: Option<PathBuf>,
211
212 #[clap(
215 long,
216 value_name = "DIR",
217 conflicts_with_all = ["deprecated_memory_backing_file", "numa"]
218 )]
219 pub restore_snapshot: Option<PathBuf>,
220
221 #[clap(long = "private-memory", hide = true, conflicts_with_all = ["deprecated_memory_backing_file", "restore_snapshot", "numa"])]
223 pub deprecated_private_memory: bool,
224
225 #[clap(long = "thp", hide = true, conflicts_with = "numa")]
227 pub deprecated_thp: bool,
228
229 #[clap(short = 'P', long)]
231 pub paused: bool,
232
233 #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
235 pub kernel: OptionalPathBuf,
236
237 #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
239 pub initrd: OptionalPathBuf,
240
241 #[clap(short = 'c', long, value_name = "STRING")]
243 pub cmdline: Vec<String>,
244
245 #[clap(long)]
247 pub hv: bool,
248
249 #[clap(long, conflicts_with_all = ["uefi", "pcat", "igvm"])]
253 pub device_tree: bool,
254
255 #[clap(long, requires("hv"))]
259 pub vtl2: bool,
260
261 #[clap(long, requires("hv"))]
264 pub get: bool,
265
266 #[clap(long, conflicts_with("get"))]
269 pub no_get: bool,
270
271 #[clap(
273 long,
274 conflicts_with_all = [
275 "vmbus_vsock_path",
276 "vmbus_vtl2_vsock_path",
277 "vmbus_redirect",
278 "vmbus_max_version",
279 "vmbus_com1_serial",
280 "vmbus_com2_serial",
281 "disk",
282 "vtl2",
283 "get",
284 "pcat",
285 ],
286 )]
287 pub no_vmbus: bool,
288
289 #[clap(long, requires("vtl2"))]
291 pub no_alias_map: bool,
292
293 #[clap(long, requires("vtl2"))]
295 pub isolation: Option<IsolationCli>,
296
297 #[clap(long, value_name = "PATH", alias = "vsock-path")]
299 pub vmbus_vsock_path: Option<String>,
300
301 #[clap(long, value_name = "PATH", requires("vtl2"), alias = "vtl2-vsock-path")]
303 pub vmbus_vtl2_vsock_path: Option<String>,
304
305 #[clap(long, requires("vtl2"), default_value = "halt")]
307 pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
308
309 #[clap(long_help = r#"
311e.g: --disk memdiff:file:/path/to/disk.vhd
312
313syntax: <path> | kind:<arg>[,flag,opt=arg,...]
314
315valid disk kinds:
316 `mem:<len>` memory backed disk
317 <len>: length of ramdisk, e.g.: `1G`
318 `memdiff:<disk>` memory backed diff disk
319 <disk>: lower disk, e.g.: `file:base.img`
320 `file:<path>[;direct][;create=<len>]` file-backed disk
321 <path>: path to file
322 `;direct`: bypass the OS page cache
323 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
324 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
325 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
326 `blob:<type>:<url>` HTTP blob (read-only)
327 <type>: `flat` or `vhd1`
328 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
329 <cipher>: `xts-aes-256`
330 `prwrap:<disk>` persistent reservations wrapper
331
332flags:
333 `ro` open disk as read-only
334 `dvd` specifies that device is cd/dvd and it is read_only
335 `vtl2` assign this disk to VTL2
336 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
337 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
338
339options:
340 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
341 `on=<name>` attach to a named controller (NVMe or SCSI), incompatible with `pcie_port` and `vtl2`
342 `nsid=<N>` NVMe namespace ID (1-based), requires `on`; auto-assigned if omitted
343 `lun=<N>` SCSI LUN (0-based), requires `on`; auto-assigned if omitted
344 `relay=<ctrl>[:<loc>]` relay through OpenHCL to the named OpenHCL controller, with optional location (LUN or NSID)
345"#)]
346 #[clap(long, value_name = "FILE")]
347 pub disk: Vec<DiskCli>,
348
349 #[clap(long_help = r#"
353e.g: --nvme memdiff:file:/path/to/disk.vhd
354
355syntax: <path> | kind:<arg>[,flag,opt=arg,...]
356
357valid disk kinds:
358 `mem:<len>` memory backed disk
359 <len>: length of ramdisk, e.g.: `1G`
360 `memdiff:<disk>` memory backed diff disk
361 <disk>: lower disk, e.g.: `file:base.img`
362 `file:<path>[;direct][;create=<len>]` file-backed disk
363 <path>: path to file
364 `;direct`: bypass the OS page cache
365 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
366 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
367 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
368 `blob:<type>:<url>` HTTP blob (read-only)
369 <type>: `flat` or `vhd1`
370 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
371 <cipher>: `xts-aes-256`
372 `prwrap:<disk>` persistent reservations wrapper
373
374flags:
375 `ro` open disk as read-only
376 `vtl2` assign this disk to VTL2
377 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
378 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
379
380options:
381 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
382"#)]
383 #[clap(long)]
384 pub nvme: Vec<DiskCli>,
385
386 #[clap(long_help = r#"
388Create a named NVMe controller with an explicit transport.
389
390syntax: id=<name>,pcie_port=<port> | id=<name>,vpci[=<guid>]
391
392The controller name can be referenced by `--disk` with the `on=<name>`
393option to attach namespaces to this controller.
394
395options:
396 `id=<name>` controller name (required)
397 `pcie_port=<port>` present on PCIe under the specified port
398 `vpci[=<guid>]` present via VPCI; optional instance GUID
399 `vtl2` assign to VTL2 (default VTL0)
400
401Exactly one of `pcie_port` or `vpci` must be specified.
402
403Examples:
404 --nvme-pci id=nvme0,pcie_port=p0
405 --nvme-pci id=nvme1,vpci
406 --nvme-pci id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c
407"#)]
408 #[clap(long = "nvme-pci")]
409 pub nvme_pci: Vec<NvmeControllerCli>,
410
411 #[clap(long_help = r#"
413Create a named VMBus SCSI controller.
414
415syntax: id=<name>[,sub_channels=<N>][,vtl2]
416
417The controller name can be referenced by `--disk` with the `on=<name>`
418option to attach disks to this controller.
419
420options:
421 `id=<name>` controller name (required)
422 `sub_channels=<N>` number of sub-channels (default 0)
423 `vtl2` assign to VTL2 (default VTL0)
424
425Examples:
426 --vmbus-scsi id=scsi0
427 --vmbus-scsi id=scsi1,sub_channels=4
428"#)]
429 #[clap(long = "vmbus-scsi")]
430 pub vmbus_scsi: Vec<ScsiControllerCli>,
431
432 #[clap(long_help = r#"
434Register an OpenHCL-managed storage controller that can be used as a
435relay target with `--disk ... relay=<name>`.
436
437syntax: id=<name>,type=scsi|nvme[,guid=<guid>]
438
439options:
440 `id=<name>` controller name (required)
441 `type=scsi|nvme` controller protocol (required)
442 `guid=<guid>` instance GUID (auto-derived from name if omitted)
443
444Examples:
445 --openhcl-controller id=vtl0-scsi,type=scsi
446 --openhcl-controller id=vtl0-nvme,type=nvme,guid=09a59b81-...
447"#)]
448 #[clap(long = "openhcl-controller")]
449 pub openhcl_controller: Vec<OpenhclControllerCli>,
450
451 #[clap(long = "cxl-test", value_name = "mem:<len>,pcie_port=<name>")]
453 pub cxl_test: Vec<CxlTestDeviceCli>,
454
455 #[clap(long_help = r#"
457e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
458
459syntax: <path> | kind:<arg>[,flag,opt=arg,...]
460
461valid disk kinds:
462 `mem:<len>` memory backed disk
463 <len>: length of ramdisk, e.g.: `1G`
464 `memdiff:<disk>` memory backed diff disk
465 <disk>: lower disk, e.g.: `file:base.img`
466 `file:<path>[;direct]` file-backed disk
467 <path>: path to file
468 `;direct`: bypass the OS page cache
469
470flags:
471 `ro` open disk as read-only
472
473options:
474 `pcie_port=<name>` present the disk using pcie under the specified port
475"#)]
476 #[clap(long = "virtio-blk")]
477 pub virtio_blk: Vec<DiskCli>,
478
479 #[cfg(target_os = "linux")]
504 #[clap(long = "vhost-user")]
505 pub vhost_user: Vec<VhostUserCli>,
506
507 #[clap(long, value_name = "COUNT", default_value = "0")]
509 pub scsi_sub_channels: u16,
510
511 #[clap(long)]
513 pub nic: bool,
514
515 #[clap(long)]
527 pub net: Vec<NicConfigCli>,
528
529 #[clap(long, value_name = "SWITCH_ID")]
533 pub kernel_vmnic: Vec<String>,
534
535 #[clap(long)]
537 pub gfx: bool,
538
539 #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
541 pub vtl2_gfx: bool,
542
543 #[clap(flatten)]
545 pub vnc: VncCli,
546
547 #[cfg(guest_arch = "x86_64")]
549 #[clap(long, default_value_t)]
550 pub apic_id_offset: u32,
551
552 #[clap(long)]
554 pub vps_per_socket: Option<u32>,
555
556 #[clap(long, default_value = "auto")]
558 pub smt: SmtConfigCli,
559
560 #[cfg(guest_arch = "x86_64")]
562 #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
563 pub x2apic: X2ApicConfig,
564
565 #[cfg(guest_arch = "aarch64")]
567 #[clap(long, default_value = "auto")]
568 pub gic_msi: GicMsiCli,
569
570 #[cfg(guest_arch = "aarch64")]
572 #[clap(long, value_name = "RC_NAME")]
573 pub smmu: Vec<String>,
574
575 #[clap(long, value_name = "SERIAL")]
577 pub com1: Option<SerialConfigCli>,
578
579 #[clap(long, value_name = "SERIAL")]
581 pub com2: Option<SerialConfigCli>,
582
583 #[clap(long, value_name = "SERIAL")]
585 pub com3: Option<SerialConfigCli>,
586
587 #[clap(long, value_name = "SERIAL")]
589 pub com4: Option<SerialConfigCli>,
590
591 #[structopt(long, value_name = "SERIAL")]
593 pub vmbus_com1_serial: Option<SerialConfigCli>,
594
595 #[structopt(long, value_name = "SERIAL")]
597 pub vmbus_com2_serial: Option<SerialConfigCli>,
598
599 #[clap(long)]
601 pub serial_tx_only: bool,
602
603 #[clap(long, value_name = "SERIAL")]
605 pub debugcon: Option<DebugconSerialConfigCli>,
606
607 #[clap(long, short = 'e')]
609 pub uefi: bool,
610
611 #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
613 pub uefi_firmware: OptionalPathBuf,
614
615 #[clap(long, requires("uefi"))]
617 pub uefi_debug: bool,
618
619 #[clap(long, requires("uefi"))]
621 pub uefi_enable_memory_protections: bool,
622
623 #[clap(long, requires("pcat"))]
634 pub pcat_boot_order: Option<PcatBootOrderCli>,
635
636 #[clap(long, conflicts_with("uefi"))]
638 pub pcat: bool,
639
640 #[clap(long, requires("pcat"), value_name = "FILE")]
642 pub pcat_firmware: Option<PathBuf>,
643
644 #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
646 pub igvm: Option<PathBuf>,
647
648 #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
651 pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
652
653 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
658 pub virtio_9p: Vec<FsArgs>,
659
660 #[clap(long)]
662 pub virtio_9p_debug: bool,
663
664 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
669 pub virtio_fs: Vec<FsArgsWithOptions>,
670
671 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
676 pub virtio_fs_shmem: Vec<FsArgs>,
677
678 #[clap(long, value_name = "BUS", default_value = "auto")]
680 pub virtio_fs_bus: VirtioBusCli,
681
682 #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
687 pub virtio_pmem: Option<VirtioPmemArgs>,
688
689 #[clap(long)]
691 pub virtio_rng: bool,
692
693 #[clap(long, value_name = "BUS", default_value = "auto")]
695 pub virtio_rng_bus: VirtioBusCli,
696
697 #[clap(long, value_name = "PORT", requires("virtio_rng"))]
699 pub virtio_rng_pcie_port: Option<String>,
700
701 #[clap(long)]
707 pub virtio_console: Option<SerialConfigCli>,
708
709 #[clap(long, value_name = "PORT", requires("virtio_console"))]
711 pub virtio_console_pcie_port: Option<String>,
712
713 #[clap(long, value_name = "PATH")]
715 pub virtio_vsock_path: Option<String>,
716
717 #[clap(long)]
724 pub virtio_net: Vec<NicConfigCli>,
725
726 #[clap(long, value_name = "PATH")]
728 pub log_file: Option<PathBuf>,
729
730 #[clap(long, value_name = "PATH")]
734 pub pidfile: Option<PathBuf>,
735
736 #[clap(long, value_name = "SOCKETPATH")]
738 pub ttrpc: Option<PathBuf>,
739
740 #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
742 pub grpc: Option<PathBuf>,
743
744 #[clap(long)]
746 pub single_process: bool,
747
748 #[cfg(windows)]
750 #[clap(long, value_name = "PATH")]
751 pub device: Vec<String>,
752
753 #[clap(long, requires("uefi"))]
755 pub disable_frontpage: bool,
756
757 #[clap(long)]
759 pub tpm: bool,
760
761 #[clap(long, default_value = "control", hide(true))]
765 #[expect(clippy::option_option)]
766 pub internal_worker: Option<Option<String>>,
767
768 #[clap(long, requires("vtl2"))]
770 pub vmbus_redirect: bool,
771
772 #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
774 pub vmbus_max_version: Option<u32>,
775
776 #[clap(long_help = r#"
780e.g: --vmgs memdiff:file:/path/to/file.vmgs
781
782syntax: <path> | kind:<arg>[,flag]
783
784valid disk kinds:
785 `mem:<len>` memory backed disk
786 <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
787 `memdiff:<disk>[;create=<len>]` memory backed diff disk
788 <disk>: lower disk, e.g.: `file:base.img`
789 `file:<path>` file-backed disk
790 <path>: path to file
791
792flags:
793 `fmt` reprovision the VMGS before boot
794 `fmt-on-fail` reprovision the VMGS before boot if it is corrupted
795"#)]
796 #[clap(long)]
797 pub vmgs: Option<VmgsCli>,
798
799 #[clap(long, requires("vmgs"))]
801 pub test_gsp_by_id: bool,
802
803 #[clap(long, requires("pcat"), value_name = "FILE")]
805 pub vga_firmware: Option<PathBuf>,
806
807 #[clap(long)]
809 pub secure_boot: bool,
810
811 #[clap(long)]
813 pub secure_boot_template: Option<SecureBootTemplateCli>,
814
815 #[clap(long, value_name = "PATH")]
817 pub custom_uefi_json: Option<PathBuf>,
818
819 #[clap(long, hide(true))]
824 pub relay_console_path: Option<PathBuf>,
825
826 #[clap(long, hide(true))]
830 pub relay_console_title: Option<String>,
831
832 #[clap(long, value_name = "PORT")]
834 pub gdb: Option<u16>,
835
836 #[clap(long)]
841 pub mana: Vec<NicConfigCli>,
842
843 #[clap(long)]
869 pub hypervisor: Option<String>,
870
871 #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
879 pub custom_dsdt: Option<PathBuf>,
880
881 #[clap(long_help = r#"
891e.g: --ide memdiff:file:/path/to/disk.vhd
892
893syntax: <path> | kind:<arg>[,flag,opt=arg,...]
894
895valid disk kinds:
896 `mem:<len>` memory backed disk
897 <len>: length of ramdisk, e.g.: `1G`
898 `memdiff:<disk>` memory backed diff disk
899 <disk>: lower disk, e.g.: `file:base.img`
900 `file:<path>[;create=<len>]` file-backed disk
901 <path>: path to file
902 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
903 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
904 `blob:<type>:<url>` HTTP blob (read-only)
905 <type>: `flat` or `vhd1`
906 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
907 <cipher>: `xts-aes-256`
908
909additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
910this list is not exhaustive.
911
912flags:
913 `ro` open disk as read-only
914 `s` attach drive to secondary ide channel
915 `dvd` specifies that device is cd/dvd and it is read_only
916"#)]
917 #[clap(long, value_name = "FILE", requires("pcat"))]
918 pub ide: Vec<IdeDiskCli>,
919
920 #[clap(long_help = r#"
923e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
924
925syntax: <path> | kind:<arg>[,flag,opt=arg,...]
926
927valid disk kinds:
928 `mem:<len>` memory backed disk
929 <len>: length of ramdisk, e.g.: `1G`
930 `memdiff:<disk>` memory backed diff disk
931 <disk>: lower disk, e.g.: `file:base.img`
932 `file:<path>[;create=<len>]` file-backed disk
933 <path>: path to file
934 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
935 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
936 `blob:<type>:<url>` HTTP blob (read-only)
937 <type>: `flat` or `vhd1`
938 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
939 <cipher>: `xts-aes-256`
940
941flags:
942 `ro` open disk as read-only
943"#)]
944 #[clap(long, value_name = "FILE", requires("pcat"))]
945 pub floppy: Vec<FloppyDiskCli>,
946
947 #[clap(long)]
949 pub guest_watchdog: bool,
950
951 #[clap(long)]
953 pub openhcl_dump_path: Option<PathBuf>,
954
955 #[clap(long)]
957 pub halt_on_reset: bool,
958
959 #[clap(long)]
961 pub write_saved_state_proto: Option<PathBuf>,
962
963 #[clap(long)]
965 pub imc: Option<PathBuf>,
966
967 #[clap(long)]
969 pub battery: bool,
970
971 #[clap(long)]
973 pub uefi_console_mode: Option<UefiConsoleModeCli>,
974
975 #[clap(long_help = r#"
977Set the EFI diagnostics log level.
978
979options:
980 default default (ERROR and WARN only)
981 info info (ERROR, WARN, and INFO)
982 full full (all log levels)
983"#)]
984 #[clap(long, requires("uefi"))]
985 pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
986
987 #[clap(long)]
989 pub default_boot_always_attempt: bool,
990
991 #[cfg(guest_arch = "x86_64")]
997 #[clap(long)]
998 pub amd_iommu: Vec<String>,
999
1000 #[clap(long_help = r#"
1002Attach root complexes to the VM.
1003
1004Examples:
1005 # Attach root complex rc0 on segment 0 with bus and MMIO ranges
1006 --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
1007
1008 # Configure HDM window size and restrictions (bitmask)
1009 --pcie-root-complex rc1,hdm=2G,hdm_window_restrictions=0x21
1010
1011Syntax: <name>[,opt=arg,...]
1012
1013Options:
1014 `segment=<value>` configures the PCI Express segment, default 0
1015 `start_bus=<value>` lowest valid bus number, default 0
1016 `end_bus=<value>` highest valid bus number, default 255
1017 `low_mmio=<size>` low MMIO window size, default 64M
1018 `high_mmio=<size>` high MMIO window size, default 1G
1019 `hdm=<size>` HDM decoder MMIO window size (CFMWS window), default 1G
1020 `hdm_window_restrictions=<m>` CFMWS window restriction bitmask (u16, decimal or 0x-prefixed hex),
1021 default DEVICE_COHERENT (bit 0, value 0x1)
1022"#)]
1023 #[clap(long, conflicts_with("pcat"))]
1024 pub pcie_root_complex: Vec<PcieRootComplexCli>,
1025
1026 #[clap(long_help = r#"
1028Attach root ports to root complexes.
1029
1030Examples:
1031 # Attach root port rc0rp0 to root complex rc0
1032 --pcie-root-port rc0:rc0rp0
1033
1034 # Attach root port rc0rp1 to root complex rc0 with hotplug support
1035 --pcie-root-port rc0:rc0rp1,hotplug
1036
1037Syntax: <root_complex_name>:<name>[,opt,opt=arg,...]
1038
1039Options:
1040 `hotplug` enable hotplug support for this root port
1041 `acs=<mask>` ACS capability bitmask (u16, decimal or 0x-prefixed hex)
1042 `cxl` configure this root port as CXL-capable
1043"#)]
1044 #[clap(long, conflicts_with("pcat"))]
1045 pub pcie_root_port: Vec<PcieRootPortCli>,
1046
1047 #[clap(long_help = r#"
1049Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
1050
1051Examples:
1052 # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
1053 --pcie-switch rp0:switch0,num_downstream_ports=4
1054
1055 # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
1056 --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
1057
1058 # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
1059 --pcie-switch rp0:switch0
1060 --pcie-switch switch0-downstream-0:switch1
1061 --pcie-switch switch1-downstream-1:switch2
1062
1063 # Enable hotplug on all downstream switch ports of switch0
1064 --pcie-switch rp0:switch0,hotplug
1065
1066Syntax: <port_name>:<name>[,opt,opt=arg,...]
1067
1068 port_name can be:
1069 - Root port name (e.g., "rp0") to connect directly to a root port
1070 - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
1071
1072Options:
1073 `hotplug` enable hotplug support for all downstream switch ports
1074 `num_downstream_ports=<value>` number of downstream ports, default 4
1075 `acs=<mask>` ACS capability bitmask for downstream switch ports
1076"#)]
1077 #[clap(long, conflicts_with("pcat"))]
1078 pub pcie_switch: Vec<GenericPcieSwitchCli>,
1079
1080 #[clap(long_help = r#"
1082Attach PCIe devices to root ports or downstream switch ports
1083which are implemented in a simulator running in a remote process.
1084
1085Examples:
1086 # Attach to root port rc0rp0 with default socket
1087 --pcie-remote rc0rp0
1088
1089 # Attach with custom socket address
1090 --pcie-remote rc0rp0,socket=0.0.0.0:48914
1091
1092 # Specify HU and controller identifiers
1093 --pcie-remote rc0rp0,hu=1,controller=0
1094
1095 # Multiple devices on different ports
1096 --pcie-remote rc0rp0,socket=0.0.0.0:48914
1097 --pcie-remote rc0rp1,socket=0.0.0.0:48915
1098
1099Syntax: <port_name>[,opt=arg,...]
1100
1101Options:
1102 `socket=<address>` TCP socket (default: localhost:48914)
1103 `hu=<value>` Hardware unit identifier (default: 0)
1104 `controller=<value>` Controller identifier (default: 0)
1105"#)]
1106 #[clap(long, conflicts_with("pcat"))]
1107 pub pcie_remote: Vec<PcieRemoteCli>,
1108
1109 #[clap(long_help = r#"
1111Assign a host PCI device to the guest via Linux VFIO.
1112
1113The device must be bound to vfio-pci on the host before starting the VM.
1114
1115Examples:
1116 --vfio host=0000:01:00.0,port=rp0
1117 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1118
1119Keys:
1120 host=<pci_bdf> (required) PCI address on the host
1121 port=<name> (required) Root port or downstream switch port name
1122 iommu=<id> (optional) Reference to an --iommu object. When present,
1123 uses VFIO cdev + iommufd instead of the legacy group path.
1124"#)]
1125 #[cfg(target_os = "linux")]
1126 #[clap(long, conflicts_with("pcat"))]
1127 pub vfio: Vec<VfioDeviceCli>,
1128
1129 #[clap(long_help = r#"
1131Declare an iommufd context. Opens /dev/iommu so it can be referenced by
1132--vfio devices via the iommu=<id> key. The associated IOAS is allocated
1133the first time a --vfio device referring to this id is opened.
1134
1135Requires Linux kernel >= 6.6 with iommufd support.
1136
1137Examples:
1138 --iommu id=iommu0 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1139
1140Syntax: id=<name>
1141"#)]
1142 #[cfg(target_os = "linux")]
1143 #[clap(long, conflicts_with("pcat"))]
1144 pub iommu: Vec<IommuCli>,
1145}
1146
1147impl Options {
1148 pub fn memory_size(&self) -> u64 {
1150 self.memory.mem_size
1151 }
1152
1153 pub fn prefetch_memory(&self) -> bool {
1155 self.memory.prefetch || self.deprecated_prefetch
1156 }
1157
1158 pub fn private_memory(&self) -> bool {
1160 self.memory.shared == Some(false) || self.deprecated_private_memory
1161 }
1162
1163 pub fn transparent_hugepages(&self) -> bool {
1165 self.memory.transparent_hugepages || self.deprecated_thp
1166 }
1167
1168 pub fn memory_backing_file(&self) -> Option<&PathBuf> {
1170 self.memory
1171 .file
1172 .as_ref()
1173 .or(self.deprecated_memory_backing_file.as_ref())
1174 }
1175
1176 pub fn validate_memory_options(&self) -> anyhow::Result<()> {
1178 if self.memory.file.is_some() && self.deprecated_memory_backing_file.is_some() {
1179 anyhow::bail!("--memory file=... conflicts with --memory-backing-file");
1180 }
1181 if self.memory.file.is_some() && self.restore_snapshot.is_some() {
1182 anyhow::bail!("--memory file=... conflicts with --restore-snapshot");
1183 }
1184 if self.memory.shared == Some(true) && self.deprecated_private_memory {
1185 anyhow::bail!("--memory shared=on conflicts with --private-memory");
1186 }
1187 if self.memory_backing_file().is_some() && self.private_memory() {
1188 anyhow::bail!("file-backed memory conflicts with private memory");
1189 }
1190 if self.transparent_hugepages() && !self.private_memory() {
1191 anyhow::bail!("transparent huge pages requires private memory mode");
1192 }
1193 if self.memory.hugepages {
1194 if !cfg!(target_os = "linux") {
1195 anyhow::bail!("hugepages are only supported on Linux");
1196 }
1197 if self.private_memory() {
1198 anyhow::bail!("hugepages conflict with private memory");
1199 }
1200 if self.memory_backing_file().is_some() || self.restore_snapshot.is_some() {
1201 anyhow::bail!("hugepages conflict with file-backed memory");
1202 }
1203 if self.pcat {
1204 anyhow::bail!("hugepages conflict with x86 legacy RAM splitting");
1205 }
1206 }
1207 Ok(())
1208 }
1209}
1210
1211#[derive(Clone, Debug, PartialEq)]
1212pub struct FsArgs {
1213 pub tag: String,
1214 pub path: String,
1215 pub pcie_port: Option<String>,
1216}
1217
1218impl FromStr for FsArgs {
1219 type Err = anyhow::Error;
1220
1221 fn from_str(s: &str) -> Result<Self, Self::Err> {
1222 let (pcie_port, s) = parse_pcie_port_prefix(s);
1223 let mut s = s.split(',');
1224 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
1225 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
1226 };
1227 Ok(Self {
1228 tag: tag.to_owned(),
1229 path: path.to_owned(),
1230 pcie_port,
1231 })
1232 }
1233}
1234
1235#[derive(Clone, Debug, PartialEq)]
1236pub struct FsArgsWithOptions {
1237 pub tag: String,
1239 pub path: String,
1241 pub options: String,
1243 pub pcie_port: Option<String>,
1245}
1246
1247impl FromStr for FsArgsWithOptions {
1248 type Err = anyhow::Error;
1249
1250 fn from_str(s: &str) -> Result<Self, Self::Err> {
1251 let (pcie_port, s) = parse_pcie_port_prefix(s);
1252 let mut s = s.split(',');
1253 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
1254 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
1255 };
1256 let options = s.collect::<Vec<_>>().join(";");
1257 Ok(Self {
1258 tag: tag.to_owned(),
1259 path: path.to_owned(),
1260 options,
1261 pcie_port,
1262 })
1263 }
1264}
1265
1266#[derive(Copy, Clone, clap::ValueEnum)]
1267pub enum VirtioBusCli {
1268 Auto,
1269 Mmio,
1270 Pci,
1271 Vpci,
1272}
1273
1274fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
1279 if let Some(rest) = s.strip_prefix("pcie_port=") {
1280 if let Some((port, rest)) = rest.split_once(':') {
1281 if !port.is_empty() {
1282 return (Some(port.to_string()), rest);
1283 }
1284 }
1285 }
1286 (None, s)
1287}
1288
1289#[derive(Clone, Debug, PartialEq)]
1290pub struct VirtioPmemArgs {
1291 pub path: String,
1292 pub pcie_port: Option<String>,
1293}
1294
1295impl FromStr for VirtioPmemArgs {
1296 type Err = anyhow::Error;
1297
1298 fn from_str(s: &str) -> Result<Self, Self::Err> {
1299 let (pcie_port, s) = parse_pcie_port_prefix(s);
1300 if s.is_empty() {
1301 anyhow::bail!("expected [pcie_port=<port>:]<path>");
1302 }
1303 Ok(Self {
1304 path: s.to_owned(),
1305 pcie_port,
1306 })
1307 }
1308}
1309
1310#[derive(clap::ValueEnum, Clone, Copy)]
1311pub enum SecureBootTemplateCli {
1312 Windows,
1313 UefiCa,
1314}
1315
1316fn parse_memory(s: &str) -> anyhow::Result<u64> {
1317 if s == "VMGS_DEFAULT" {
1318 Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
1319 } else {
1320 || -> Option<u64> {
1321 let mut b = s.as_bytes();
1322 if s.ends_with('B') {
1323 b = &b[..b.len() - 1]
1324 }
1325 if b.is_empty() {
1326 return None;
1327 }
1328 let multi = match b[b.len() - 1] as char {
1329 'T' => Some(1024 * 1024 * 1024 * 1024),
1330 'G' => Some(1024 * 1024 * 1024),
1331 'M' => Some(1024 * 1024),
1332 'K' => Some(1024),
1333 _ => None,
1334 };
1335 if multi.is_some() {
1336 b = &b[..b.len() - 1]
1337 }
1338 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
1339 n.checked_mul(multi.unwrap_or(1))
1340 }()
1341 .with_context(|| format!("invalid memory size '{0}'", s))
1342 }
1343}
1344
1345fn parse_acs_capability_mask(value: &str) -> anyhow::Result<u16> {
1346 if let Some(hex) = value
1347 .strip_prefix("0x")
1348 .or_else(|| value.strip_prefix("0X"))
1349 {
1350 u16::from_str_radix(hex, 16).context("invalid ACS capability mask")
1351 } else {
1352 value.parse::<u16>().context("invalid ACS capability mask")
1353 }
1354}
1355
1356fn parse_memory_toggle(key: &str, value: &str) -> anyhow::Result<bool> {
1357 match value {
1358 "on" => Ok(true),
1359 "off" => Ok(false),
1360 _ => anyhow::bail!("invalid {key} value '{value}', expected 'on' or 'off'"),
1361 }
1362}
1363
1364#[derive(Default)]
1368struct MemoryOptionAccum {
1369 mem_size: Option<u64>,
1370 shared: Option<bool>,
1371 prefetch: Option<bool>,
1372 transparent_hugepages: Option<bool>,
1373 hugepages: Option<bool>,
1374 hugepage_size: Option<u64>,
1375}
1376
1377impl MemoryOptionAccum {
1378 fn try_parse(&mut self, key: &str, value: &str) -> anyhow::Result<bool> {
1381 match key {
1382 "size" => {
1383 anyhow::ensure!(self.mem_size.is_none(), "duplicate option 'size'");
1384 self.mem_size = Some(parse_memory(value)?);
1385 }
1386 "shared" => {
1387 anyhow::ensure!(self.shared.is_none(), "duplicate option 'shared'");
1388 self.shared = Some(parse_memory_toggle(key, value)?);
1389 }
1390 "prefetch" => {
1391 anyhow::ensure!(self.prefetch.is_none(), "duplicate option 'prefetch'");
1392 self.prefetch = Some(parse_memory_toggle(key, value)?);
1393 }
1394 "thp" => {
1395 anyhow::ensure!(
1396 self.transparent_hugepages.is_none(),
1397 "duplicate option 'thp'"
1398 );
1399 self.transparent_hugepages = Some(parse_memory_toggle(key, value)?);
1400 }
1401 "hugepages" => {
1402 anyhow::ensure!(self.hugepages.is_none(), "duplicate option 'hugepages'");
1403 self.hugepages = Some(parse_memory_toggle(key, value)?);
1404 }
1405 "hugepage_size" => {
1406 anyhow::ensure!(
1407 self.hugepage_size.is_none(),
1408 "duplicate option 'hugepage_size'"
1409 );
1410 self.hugepage_size = Some(parse_memory(value)?);
1411 }
1412 _ => return Ok(false),
1413 }
1414 Ok(true)
1415 }
1416
1417 fn finish(self, default_size: u64, file: Option<PathBuf>) -> anyhow::Result<MemoryCli> {
1419 if self.transparent_hugepages == Some(true) && self.shared != Some(false) {
1420 anyhow::bail!("thp=on requires shared=off");
1421 }
1422 if self.hugepage_size.is_some() && self.hugepages != Some(true) {
1423 anyhow::bail!("hugepage_size requires hugepages=on");
1424 }
1425 if self.hugepages == Some(true) {
1426 if self.shared == Some(false) {
1427 anyhow::bail!("hugepages=on conflicts with shared=off");
1428 }
1429 if file.is_some() {
1430 anyhow::bail!("hugepages=on conflicts with file=...");
1431 }
1432 }
1433 Ok(MemoryCli {
1434 mem_size: self.mem_size.unwrap_or(default_size),
1435 shared: self.shared,
1436 prefetch: self.prefetch.unwrap_or(false),
1437 transparent_hugepages: self.transparent_hugepages.unwrap_or(false),
1438 hugepages: self.hugepages.unwrap_or(false),
1439 hugepage_size: self.hugepage_size,
1440 file,
1441 })
1442 }
1443}
1444
1445fn parse_memory_config(s: &str) -> anyhow::Result<MemoryCli> {
1446 if !s.contains('=') && !s.contains(',') {
1447 return Ok(MemoryCli {
1448 mem_size: parse_memory(s)?,
1449 shared: None,
1450 prefetch: false,
1451 transparent_hugepages: false,
1452 hugepages: false,
1453 hugepage_size: None,
1454 file: None,
1455 });
1456 }
1457
1458 let mut accum = MemoryOptionAccum::default();
1459 let mut file = None;
1460
1461 for part in s.split(',') {
1462 let (key, value) = part
1463 .split_once('=')
1464 .with_context(|| format!("invalid memory option '{part}', expected key=value"))?;
1465 if key.is_empty() || value.is_empty() {
1466 anyhow::bail!("invalid memory option '{part}', expected key=value");
1467 }
1468
1469 if accum.try_parse(key, value)? {
1470 continue;
1471 }
1472 match key {
1473 "file" => {
1474 anyhow::ensure!(file.is_none(), "duplicate memory option 'file'");
1475 file = Some(PathBuf::from(value));
1476 }
1477 _ => anyhow::bail!("unknown memory option '{key}'"),
1478 }
1479 }
1480
1481 accum.finish(DEFAULT_MEMORY_SIZE, file)
1482}
1483
1484fn split_options(s: &str) -> anyhow::Result<Vec<&str>> {
1486 let mut parts = Vec::new();
1487 let mut depth = 0u32;
1488 let mut start = 0;
1489 for (i, c) in s.char_indices() {
1490 match c {
1491 '[' => depth += 1,
1492 ']' => {
1493 anyhow::ensure!(depth > 0, "unmatched ']' in '{s}'");
1494 depth -= 1;
1495 }
1496 ',' if depth == 0 => {
1497 parts.push(&s[start..i]);
1498 start = i + 1;
1499 }
1500 _ => {}
1501 }
1502 }
1503 anyhow::ensure!(depth == 0, "unmatched '[' in '{s}'");
1504 parts.push(&s[start..]);
1505 Ok(parts)
1506}
1507
1508fn parse_vp_list(value: &str) -> anyhow::Result<Vec<u32>> {
1511 let inner = value
1512 .strip_prefix('[')
1513 .and_then(|s| s.strip_suffix(']'))
1514 .with_context(|| {
1515 format!("vps value must use bracket syntax, e.g. [0,1,2-3], got '{value}'")
1516 })?;
1517
1518 if inner.is_empty() {
1519 return Ok(Vec::new());
1520 }
1521
1522 let mut vps = Vec::new();
1523 for item in inner.split(',') {
1524 let item = item.trim();
1525 if let Some((lo, hi)) = item.split_once('-') {
1526 let lo = lo.trim().parse::<u32>().context("invalid vp index")?;
1527 let hi = hi.trim().parse::<u32>().context("invalid vp index")?;
1528 anyhow::ensure!(lo <= hi, "invalid vp range {lo}-{hi}");
1529 vps.extend(lo..=hi);
1530 } else {
1531 vps.push(item.parse::<u32>().context("invalid vp index")?);
1532 }
1533 }
1534 Ok(vps)
1535}
1536
1537fn parse_numa_node(s: &str) -> anyhow::Result<NumaNodeCli> {
1538 let mut accum = MemoryOptionAccum::default();
1539 let mut host_numa_node = None;
1540 let mut vps: Option<Vec<u32>> = None;
1541
1542 for part in split_options(s)? {
1543 let (key, value) = part
1544 .split_once('=')
1545 .with_context(|| format!("invalid numa option '{part}', expected key=value"))?;
1546
1547 if accum.try_parse(key, value)? {
1548 continue;
1549 }
1550 match key {
1551 "host_numa_node" => {
1552 anyhow::ensure!(
1553 host_numa_node.is_none(),
1554 "duplicate numa option 'host_numa_node'"
1555 );
1556 host_numa_node = Some(value.parse::<u32>().context("invalid host_numa_node")?);
1557 }
1558 "vps" => {
1559 anyhow::ensure!(vps.is_none(), "duplicate numa option 'vps'");
1560 vps = Some(parse_vp_list(value)?);
1561 }
1562 _ => anyhow::bail!("unknown numa option '{key}'"),
1563 }
1564 }
1565
1566 anyhow::ensure!(accum.mem_size.is_some(), "numa node requires 'size' option");
1567 let memory = accum.finish(0, None)?;
1568
1569 Ok(NumaNodeCli {
1570 memory,
1571 host_numa_node,
1572 vps,
1573 })
1574}
1575
1576fn parse_numa_distance(s: &str) -> anyhow::Result<NumaDistanceCli> {
1577 let parts: Vec<&str> = s.split(':').collect();
1578 anyhow::ensure!(
1579 parts.len() == 3,
1580 "expected SRC:DST:DISTANCE format, got '{s}'"
1581 );
1582 let src = parts[0].parse::<u32>().context("invalid source node")?;
1583 let dst = parts[1]
1584 .parse::<u32>()
1585 .context("invalid destination node")?;
1586 let distance = parts[2].parse::<u8>().context("invalid distance")?;
1587 anyhow::ensure!(
1588 distance >= 10,
1589 "distance must be >= 10 (10 = local), got {distance}"
1590 );
1591 Ok(NumaDistanceCli { src, dst, distance })
1592}
1593
1594fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
1596 match s.strip_prefix("0x") {
1597 Some(rest) => u64::from_str_radix(rest, 16),
1598 None => s.parse::<u64>(),
1599 }
1600}
1601
1602#[derive(Clone, Debug, PartialEq)]
1603pub enum DiskCliKind {
1604 Memory(u64),
1606 MemoryDiff(Box<DiskCliKind>),
1608 Sqlite {
1610 path: PathBuf,
1611 create_with_len: Option<u64>,
1612 },
1613 SqliteDiff {
1615 path: PathBuf,
1616 create: bool,
1617 disk: Box<DiskCliKind>,
1618 },
1619 AutoCacheSqlite {
1621 cache_path: String,
1622 key: Option<String>,
1623 disk: Box<DiskCliKind>,
1624 },
1625 PersistentReservationsWrapper(Box<DiskCliKind>),
1627 File {
1629 path: PathBuf,
1630 create_with_len: Option<u64>,
1631 direct: bool,
1632 },
1633 Blob {
1635 kind: BlobKind,
1636 url: String,
1637 },
1638 Crypt {
1640 cipher: DiskCipher,
1641 key_file: PathBuf,
1642 disk: Box<DiskCliKind>,
1643 },
1644 DelayDiskWrapper {
1646 delay_ms: u64,
1647 disk: Box<DiskCliKind>,
1648 },
1649}
1650
1651#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
1652pub enum DiskCipher {
1653 #[clap(name = "xts-aes-256")]
1654 XtsAes256,
1655}
1656
1657#[derive(Copy, Clone, Debug, PartialEq)]
1658pub enum BlobKind {
1659 Flat,
1660 Vhd1,
1661}
1662
1663struct FileOpts {
1664 path: PathBuf,
1665 create_with_len: Option<u64>,
1666 direct: bool,
1667}
1668
1669fn parse_file_opts(arg: &str) -> anyhow::Result<FileOpts> {
1670 let mut path = arg;
1671 let mut create_with_len = None;
1672 let mut direct = false;
1673
1674 if let Some((p, rest)) = arg.split_once(';') {
1676 path = p;
1677 for opt in rest.split(';') {
1678 if let Some(len) = opt.strip_prefix("create=") {
1679 create_with_len = Some(parse_memory(len)?);
1680 } else if opt == "direct" {
1681 direct = true;
1682 } else {
1683 anyhow::bail!("invalid file option '{opt}', expected 'create=<len>' or 'direct'");
1684 }
1685 }
1686 }
1687
1688 Ok(FileOpts {
1689 path: path.into(),
1690 create_with_len,
1691 direct,
1692 })
1693}
1694
1695impl DiskCliKind {
1696 fn parse_autocache(
1699 arg: &str,
1700 cache_path: Result<String, std::env::VarError>,
1701 ) -> anyhow::Result<Self> {
1702 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
1703 let cache_path = cache_path.context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
1704 Ok(DiskCliKind::AutoCacheSqlite {
1705 cache_path,
1706 key: (!key.is_empty()).then(|| key.to_string()),
1707 disk: Box::new(kind.parse()?),
1708 })
1709 }
1710}
1711
1712impl FromStr for DiskCliKind {
1713 type Err = anyhow::Error;
1714
1715 fn from_str(s: &str) -> anyhow::Result<Self> {
1716 let disk = match s.split_once(':') {
1717 None => {
1719 let FileOpts {
1720 path,
1721 create_with_len,
1722 direct,
1723 } = parse_file_opts(s)?;
1724 DiskCliKind::File {
1725 path,
1726 create_with_len,
1727 direct,
1728 }
1729 }
1730 Some((kind, arg)) => match kind {
1731 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
1732 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
1733 "sql" => {
1734 let FileOpts {
1735 path,
1736 create_with_len,
1737 direct,
1738 } = parse_file_opts(arg)?;
1739 if direct {
1740 anyhow::bail!("'direct' is not supported for 'sql' disks");
1741 }
1742 DiskCliKind::Sqlite {
1743 path,
1744 create_with_len,
1745 }
1746 }
1747 "sqldiff" => {
1748 let (path_and_opts, kind) =
1749 arg.split_once(':').context("expected path[;opts]:kind")?;
1750 let disk = Box::new(kind.parse()?);
1751 match path_and_opts.split_once(';') {
1752 Some((path, create)) => {
1753 if create != "create" {
1754 anyhow::bail!("invalid syntax after ';', expected 'create'")
1755 }
1756 DiskCliKind::SqliteDiff {
1757 path: path.into(),
1758 create: true,
1759 disk,
1760 }
1761 }
1762 None => DiskCliKind::SqliteDiff {
1763 path: path_and_opts.into(),
1764 create: false,
1765 disk,
1766 },
1767 }
1768 }
1769 "autocache" => {
1770 Self::parse_autocache(arg, std::env::var("OPENVMM_AUTO_CACHE_PATH"))?
1771 }
1772 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
1773 "file" => {
1774 let FileOpts {
1775 path,
1776 create_with_len,
1777 direct,
1778 } = parse_file_opts(arg)?;
1779 DiskCliKind::File {
1780 path,
1781 create_with_len,
1782 direct,
1783 }
1784 }
1785 "blob" => {
1786 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
1787 let blob_kind = match blob_kind {
1788 "flat" => BlobKind::Flat,
1789 "vhd1" => BlobKind::Vhd1,
1790 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
1791 };
1792 DiskCliKind::Blob {
1793 kind: blob_kind,
1794 url: url.to_string(),
1795 }
1796 }
1797 "crypt" => {
1798 let (cipher, (key, kind)) = arg
1799 .split_once(':')
1800 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
1801 .context("expected cipher:key_file:kind")?;
1802 DiskCliKind::Crypt {
1803 cipher: ValueEnum::from_str(cipher, false)
1804 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
1805 key_file: PathBuf::from(key),
1806 disk: Box::new(kind.parse()?),
1807 }
1808 }
1809 kind => {
1810 let FileOpts {
1815 path,
1816 create_with_len,
1817 direct,
1818 } = parse_file_opts(s)?;
1819 if path.has_root() {
1820 DiskCliKind::File {
1821 path,
1822 create_with_len,
1823 direct,
1824 }
1825 } else {
1826 anyhow::bail!("invalid disk kind {kind}");
1827 }
1828 }
1829 },
1830 };
1831 Ok(disk)
1832 }
1833}
1834
1835#[derive(Clone)]
1836pub struct VmgsCli {
1837 pub kind: DiskCliKind,
1838 pub provision: ProvisionVmgs,
1839}
1840
1841#[derive(Copy, Clone)]
1842pub enum ProvisionVmgs {
1843 OnEmpty,
1844 OnFailure,
1845 True,
1846}
1847
1848impl FromStr for VmgsCli {
1849 type Err = anyhow::Error;
1850
1851 fn from_str(s: &str) -> anyhow::Result<Self> {
1852 let (kind, opt) = s
1853 .split_once(',')
1854 .map(|(k, o)| (k, Some(o)))
1855 .unwrap_or((s, None));
1856 let kind = kind.parse()?;
1857
1858 let provision = match opt {
1859 None => ProvisionVmgs::OnEmpty,
1860 Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
1861 Some("fmt") => ProvisionVmgs::True,
1862 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
1863 };
1864
1865 Ok(VmgsCli { kind, provision })
1866 }
1867}
1868
1869#[derive(clap::Args)]
1871pub struct VncCli {
1872 #[clap(long)]
1874 pub vnc: bool,
1875
1876 #[clap(long, value_name = "PORT", default_value = "5900")]
1878 pub vnc_port: u16,
1879
1880 #[clap(long, value_name = "ADDRESS", default_value = "127.0.0.1")]
1884 pub vnc_listen: String,
1885
1886 #[clap(long, value_name = "COUNT", default_value = "16")]
1888 pub vnc_max_clients: usize,
1889
1890 #[clap(long)]
1893 pub vnc_evict_oldest: bool,
1894}
1895
1896#[derive(Clone)]
1898pub struct DiskCli {
1899 pub vtl: DeviceVtl,
1900 pub kind: DiskCliKind,
1901 pub read_only: bool,
1902 pub is_dvd: bool,
1903 pub underhill: Option<UnderhillDiskSource>,
1904 pub pcie_port: Option<String>,
1905 pub controller: Option<String>,
1906 pub nsid: Option<u32>,
1907 pub lun: Option<u8>,
1908 pub relay: Option<(String, Option<u32>)>,
1909}
1910
1911#[derive(Copy, Clone)]
1912pub enum UnderhillDiskSource {
1913 Scsi,
1914 Nvme,
1915}
1916
1917impl FromStr for DiskCli {
1918 type Err = anyhow::Error;
1919
1920 fn from_str(s: &str) -> anyhow::Result<Self> {
1921 let mut opts = s.split(',');
1922 let kind = opts.next().unwrap().parse()?;
1923
1924 let mut read_only = false;
1925 let mut is_dvd = false;
1926 let mut underhill = None;
1927 let mut vtl = DeviceVtl::Vtl0;
1928 let mut pcie_port = None;
1929 let mut controller = None;
1930 let mut nsid = None;
1931 let mut lun = None;
1932 let mut relay = None;
1933 for opt in opts {
1934 let mut s = opt.split('=');
1935 let opt = s.next().unwrap();
1936 match opt {
1937 "ro" => read_only = true,
1938 "dvd" => {
1939 is_dvd = true;
1940 read_only = true;
1941 }
1942 "vtl2" => {
1943 vtl = DeviceVtl::Vtl2;
1944 }
1945 "uh" => underhill = Some(UnderhillDiskSource::Scsi),
1946 "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
1947 "pcie_port" => {
1948 let port = s.next();
1949 if port.is_none_or(|p| p.is_empty()) {
1950 anyhow::bail!("`pcie_port` requires a port name");
1951 }
1952 pcie_port = Some(String::from(port.unwrap()));
1953 }
1954 "on" => {
1955 let name = s.next();
1956 if name.is_none_or(|n| n.is_empty()) {
1957 anyhow::bail!("`on` requires a controller name");
1958 }
1959 controller = Some(String::from(name.unwrap()));
1960 }
1961 "nsid" => {
1962 let val = s.next().context("`nsid` requires a value")?;
1963 nsid = Some(val.parse::<u32>().context("invalid `nsid` value")?);
1964 }
1965 "lun" => {
1966 let val = s.next().context("`lun` requires a value")?;
1967 lun = Some(val.parse::<u8>().context("invalid `lun` value")?);
1968 }
1969 "relay" => {
1970 let val = s.next();
1971 if val.is_none_or(|v| v.is_empty()) {
1972 anyhow::bail!("`relay` requires a target controller name");
1973 }
1974 let val = val.unwrap();
1975 if let Some((name, loc)) = val.split_once(':') {
1977 let loc = loc.parse::<u32>().context("invalid relay location")?;
1978 relay = Some((name.to_string(), Some(loc)));
1979 } else {
1980 relay = Some((val.to_string(), None));
1981 }
1982 }
1983 opt => anyhow::bail!("unknown option: '{opt}'"),
1984 }
1985 }
1986
1987 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
1988 anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
1989 }
1990
1991 if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
1992 anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
1993 }
1994
1995 if controller.is_some() && pcie_port.is_some() {
1996 anyhow::bail!("`on` is incompatible with `pcie_port`");
1997 }
1998
1999 if controller.is_some() && vtl != DeviceVtl::Vtl0 {
2000 anyhow::bail!(
2001 "`vtl2` is incompatible with `on`; the controller's VTL determines placement"
2002 );
2003 }
2004
2005 if controller.is_some() && underhill.is_some() {
2006 anyhow::bail!("`on` is incompatible with `uh` and `uh-nvme`; use `relay` instead");
2007 }
2008
2009 if nsid.is_some() && controller.is_none() {
2010 anyhow::bail!("`nsid` requires `on`");
2011 }
2012
2013 if lun.is_some() && controller.is_none() {
2014 anyhow::bail!("`lun` requires `on`");
2015 }
2016
2017 if nsid.is_some() && lun.is_some() {
2018 anyhow::bail!("`nsid` and `lun` are mutually exclusive");
2019 }
2020
2021 if relay.is_some() && controller.is_none() {
2022 anyhow::bail!("`relay` requires `on`");
2023 }
2024
2025 if relay.is_some() && underhill.is_some() {
2026 anyhow::bail!("`relay` is incompatible with `uh` and `uh-nvme`");
2027 }
2028
2029 Ok(DiskCli {
2030 vtl,
2031 kind,
2032 read_only,
2033 is_dvd,
2034 underhill,
2035 pcie_port,
2036 controller,
2037 nsid,
2038 lun,
2039 relay,
2040 })
2041 }
2042}
2043
2044#[derive(Clone, Debug, PartialEq)]
2046pub enum NvmeControllerTransport {
2047 Pcie(String),
2049 Vpci(Option<Guid>),
2051}
2052
2053#[derive(Clone, Debug)]
2055pub struct NvmeControllerCli {
2056 pub id: String,
2058 pub transport: NvmeControllerTransport,
2060 pub vtl: DeviceVtl,
2062}
2063
2064impl FromStr for NvmeControllerCli {
2065 type Err = anyhow::Error;
2066
2067 fn from_str(s: &str) -> anyhow::Result<Self> {
2068 let mut id = None;
2069 let mut pcie_port = None;
2070 let mut vpci = None;
2071 let mut vpci_set = false;
2072 let mut vtl = DeviceVtl::Vtl0;
2073
2074 for part in s.split(',') {
2075 let mut kv = part.split('=');
2076 let key = kv.next().unwrap();
2077 match key {
2078 "id" => {
2079 let val = kv.next();
2080 if val.is_none_or(|v| v.is_empty()) {
2081 anyhow::bail!("`id` requires a name");
2082 }
2083 id = Some(val.unwrap().to_string());
2084 }
2085 "pcie_port" => {
2086 let val = kv.next();
2087 if val.is_none_or(|v| v.is_empty()) {
2088 anyhow::bail!("`pcie_port` requires a port name");
2089 }
2090 pcie_port = Some(val.unwrap().to_string());
2091 }
2092 "vpci" => {
2093 vpci_set = true;
2094 if let Some(val) = kv.next() {
2095 if !val.is_empty() {
2096 vpci = Some(val.parse::<Guid>().context("invalid GUID for `vpci`")?);
2097 }
2098 }
2099 }
2100 "vtl2" => {
2101 vtl = DeviceVtl::Vtl2;
2102 }
2103 other => anyhow::bail!("unknown option: '{other}'"),
2104 }
2105 }
2106
2107 let id = id.context("`id` is required")?;
2108
2109 let transport = match (pcie_port, vpci_set) {
2110 (Some(port), false) => NvmeControllerTransport::Pcie(port),
2111 (None, true) => NvmeControllerTransport::Vpci(vpci),
2112 (Some(_), true) => {
2113 anyhow::bail!("`pcie_port` and `vpci` are mutually exclusive")
2114 }
2115 (None, false) => {
2116 anyhow::bail!("one of `pcie_port` or `vpci` is required")
2117 }
2118 };
2119
2120 Ok(NvmeControllerCli { id, transport, vtl })
2121 }
2122}
2123
2124#[derive(Clone, Debug)]
2126pub struct ScsiControllerCli {
2127 pub id: String,
2129 pub sub_channels: u16,
2131 pub vtl: DeviceVtl,
2133}
2134
2135impl FromStr for ScsiControllerCli {
2136 type Err = anyhow::Error;
2137
2138 fn from_str(s: &str) -> anyhow::Result<Self> {
2139 let mut id = None;
2140 let mut sub_channels = 0u16;
2141 let mut vtl = DeviceVtl::Vtl0;
2142
2143 for part in s.split(',') {
2144 let mut kv = part.split('=');
2145 let key = kv.next().unwrap();
2146 match key {
2147 "id" => {
2148 let val = kv.next();
2149 if val.is_none_or(|v| v.is_empty()) {
2150 anyhow::bail!("`id` requires a name");
2151 }
2152 id = Some(val.unwrap().to_string());
2153 }
2154 "sub_channels" => {
2155 let val = kv.next().context("`sub_channels` requires a value")?;
2156 sub_channels = val.parse().context("invalid `sub_channels` value")?;
2157 }
2158 "vtl2" => {
2159 vtl = DeviceVtl::Vtl2;
2160 }
2161 other => anyhow::bail!("unknown option: '{other}'"),
2162 }
2163 }
2164
2165 let id = id.context("`id` is required")?;
2166
2167 Ok(ScsiControllerCli {
2168 id,
2169 sub_channels,
2170 vtl,
2171 })
2172 }
2173}
2174
2175#[derive(Copy, Clone, Debug, PartialEq)]
2177pub enum OpenhclControllerType {
2178 Scsi,
2179 Nvme,
2180}
2181
2182#[derive(Clone, Debug)]
2184pub struct OpenhclControllerCli {
2185 pub id: String,
2187 pub controller_type: OpenhclControllerType,
2189 pub guid: Option<Guid>,
2191}
2192
2193impl FromStr for OpenhclControllerCli {
2194 type Err = anyhow::Error;
2195
2196 fn from_str(s: &str) -> anyhow::Result<Self> {
2197 let mut id = None;
2198 let mut controller_type = None;
2199 let mut guid = None;
2200
2201 for part in s.split(',') {
2202 let mut kv = part.split('=');
2203 let key = kv.next().unwrap();
2204 match key {
2205 "id" => {
2206 let val = kv.next();
2207 if val.is_none_or(|v| v.is_empty()) {
2208 anyhow::bail!("`id` requires a name");
2209 }
2210 id = Some(val.unwrap().to_string());
2211 }
2212 "type" => {
2213 let val = kv.next().context("`type` requires a value")?;
2214 controller_type = Some(match val {
2215 "scsi" => OpenhclControllerType::Scsi,
2216 "nvme" => OpenhclControllerType::Nvme,
2217 other => anyhow::bail!("unknown controller type: '{other}'"),
2218 });
2219 }
2220 "guid" => {
2221 let val = kv.next().context("`guid` requires a value")?;
2222 guid = Some(val.parse::<Guid>().context("invalid GUID")?);
2223 }
2224 other => anyhow::bail!("unknown option: '{other}'"),
2225 }
2226 }
2227
2228 let id = id.context("`id` is required")?;
2229 let controller_type = controller_type.context("`type` is required")?;
2230
2231 Ok(OpenhclControllerCli {
2232 id,
2233 controller_type,
2234 guid,
2235 })
2236 }
2237}
2238
2239#[derive(Clone, Debug, PartialEq)]
2241pub struct CxlTestDeviceCli {
2242 pub hdm_size: u64,
2244 pub pcie_port: String,
2246}
2247
2248impl FromStr for CxlTestDeviceCli {
2249 type Err = anyhow::Error;
2250
2251 fn from_str(s: &str) -> anyhow::Result<Self> {
2252 let mut opts = s.split(',');
2253 let first = opts.next().context("expected CXL test device config")?;
2254 let (kind, arg) = first
2255 .split_once(':')
2256 .context("expected CXL test syntax: mem:<len>")?;
2257
2258 if kind != "mem" {
2259 anyhow::bail!("unsupported CXL test backing kind '{kind}', expected 'mem'");
2260 }
2261
2262 let hdm_size = parse_memory(arg).context("failed to parse CXL test HDM size")?;
2263 let mut pcie_port = None;
2264
2265 for opt in opts {
2266 let mut kv = opt.split('=');
2267 let key = kv.next().unwrap_or_default();
2268 match key {
2269 "pcie_port" => {
2270 let val = kv.next();
2271 if val.is_none_or(|v| v.is_empty()) {
2272 anyhow::bail!("`pcie_port` requires a port name");
2273 }
2274 pcie_port = Some(val.unwrap().to_string());
2275 }
2276 _ => anyhow::bail!("unknown option: '{opt}'"),
2277 }
2278 }
2279
2280 let Some(pcie_port) = pcie_port else {
2281 anyhow::bail!("`pcie_port=<name>` is required for `--cxl-test`");
2282 };
2283
2284 Ok(Self {
2285 hdm_size,
2286 pcie_port,
2287 })
2288 }
2289}
2290
2291#[derive(Clone)]
2293pub struct IdeDiskCli {
2294 pub kind: DiskCliKind,
2295 pub read_only: bool,
2296 pub channel: Option<u8>,
2297 pub device: Option<u8>,
2298 pub is_dvd: bool,
2299}
2300
2301impl FromStr for IdeDiskCli {
2302 type Err = anyhow::Error;
2303
2304 fn from_str(s: &str) -> anyhow::Result<Self> {
2305 let mut opts = s.split(',');
2306 let kind = opts.next().unwrap().parse()?;
2307
2308 let mut read_only = false;
2309 let mut channel = None;
2310 let mut device = None;
2311 let mut is_dvd = false;
2312 for opt in opts {
2313 let mut s = opt.split('=');
2314 let opt = s.next().unwrap();
2315 match opt {
2316 "ro" => read_only = true,
2317 "p" => channel = Some(0),
2318 "s" => channel = Some(1),
2319 "0" => device = Some(0),
2320 "1" => device = Some(1),
2321 "dvd" => {
2322 is_dvd = true;
2323 read_only = true;
2324 }
2325 _ => anyhow::bail!("unknown option: '{opt}'"),
2326 }
2327 }
2328
2329 Ok(IdeDiskCli {
2330 kind,
2331 read_only,
2332 channel,
2333 device,
2334 is_dvd,
2335 })
2336 }
2337}
2338
2339#[derive(Clone, Debug, PartialEq)]
2341pub struct FloppyDiskCli {
2342 pub kind: DiskCliKind,
2343 pub read_only: bool,
2344}
2345
2346impl FromStr for FloppyDiskCli {
2347 type Err = anyhow::Error;
2348
2349 fn from_str(s: &str) -> anyhow::Result<Self> {
2350 if s.is_empty() {
2351 anyhow::bail!("empty disk spec");
2352 }
2353 let mut opts = s.split(',');
2354 let kind = opts.next().unwrap().parse()?;
2355
2356 let mut read_only = false;
2357 for opt in opts {
2358 let mut s = opt.split('=');
2359 let opt = s.next().unwrap();
2360 match opt {
2361 "ro" => read_only = true,
2362 _ => anyhow::bail!("unknown option: '{opt}'"),
2363 }
2364 }
2365
2366 Ok(FloppyDiskCli { kind, read_only })
2367 }
2368}
2369
2370#[derive(Clone)]
2371pub struct DebugconSerialConfigCli {
2372 pub port: u16,
2373 pub serial: SerialConfigCli,
2374}
2375
2376impl FromStr for DebugconSerialConfigCli {
2377 type Err = String;
2378
2379 fn from_str(s: &str) -> Result<Self, Self::Err> {
2380 let Some((port, serial)) = s.split_once(',') else {
2381 return Err("invalid format (missing comma between port and serial)".into());
2382 };
2383
2384 let port: u16 = parse_number(port)
2385 .map_err(|_| "could not parse port".to_owned())?
2386 .try_into()
2387 .map_err(|_| "port must be 16-bit")?;
2388 let serial: SerialConfigCli = serial.parse()?;
2389
2390 Ok(Self { port, serial })
2391 }
2392}
2393
2394#[derive(Clone, Debug, PartialEq)]
2396pub enum SerialConfigCli {
2397 None,
2398 Console,
2399 NewConsole(Option<PathBuf>, Option<String>),
2400 Stderr,
2401 Pipe(PathBuf),
2402 Tcp(SocketAddr),
2403 File(PathBuf),
2404}
2405
2406impl FromStr for SerialConfigCli {
2407 type Err = String;
2408
2409 fn from_str(s: &str) -> Result<Self, Self::Err> {
2410 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
2411
2412 let first_key = match keyvalues.first() {
2413 Some(first_pair) => first_pair.0.as_str(),
2414 None => Err("invalid serial configuration: no values supplied")?,
2415 };
2416 let first_value = keyvalues.first().unwrap().1.as_ref();
2417
2418 let ret = match first_key {
2419 "none" => SerialConfigCli::None,
2420 "console" => SerialConfigCli::Console,
2421 "stderr" => SerialConfigCli::Stderr,
2422 "file" => match first_value {
2423 Some(path) => SerialConfigCli::File(path.into()),
2424 None => Err("invalid serial configuration: file requires a value")?,
2425 },
2426 "term" => {
2427 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
2429 let window_name = match window_name {
2430 Some((_, Some(name))) => Some(name.clone()),
2431 _ => None,
2432 };
2433
2434 SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
2435 }
2436 "listen" => match first_value {
2437 Some(path) => {
2438 if let Some(tcp) = path.strip_prefix("tcp:") {
2439 let addr = tcp
2440 .parse()
2441 .map_err(|err| format!("invalid tcp address: {err}"))?;
2442 SerialConfigCli::Tcp(addr)
2443 } else {
2444 SerialConfigCli::Pipe(path.into())
2445 }
2446 }
2447 None => Err(
2448 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
2449 )?,
2450 },
2451 _ => {
2452 return Err(format!(
2453 "invalid serial configuration: '{}' is not a known option",
2454 first_key
2455 ));
2456 }
2457 };
2458
2459 Ok(ret)
2460 }
2461}
2462
2463impl SerialConfigCli {
2464 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
2467 let mut ret = Vec::new();
2468
2469 for item in s.split(',') {
2471 let mut eqsplit = item.split('=');
2474 let key = eqsplit.next();
2475 let value = eqsplit.next();
2476
2477 if let Some(key) = key {
2478 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
2479 } else {
2480 return Err("invalid key=value pair in serial config".into());
2482 }
2483 }
2484 Ok(ret)
2485 }
2486}
2487
2488#[derive(Clone, Debug, PartialEq)]
2489pub enum EndpointConfigCli {
2490 None,
2491 Consomme {
2492 cidr: Option<String>,
2493 host_fwd: Vec<HostPortConfigCli>,
2494 },
2495 Dio {
2496 id: Option<String>,
2497 },
2498 Tap {
2499 name: String,
2500 },
2501}
2502
2503#[derive(Clone, Debug, PartialEq)]
2505pub struct HostPortConfigCli {
2506 pub protocol: HostPortProtocolCli,
2507 pub host_address: Option<std::net::IpAddr>,
2508 pub host_port: u16,
2509 pub guest_port: u16,
2510}
2511
2512#[derive(Clone, Debug, PartialEq)]
2514pub enum HostPortProtocolCli {
2515 Tcp,
2516 Udp,
2517}
2518
2519fn parse_hostfwd(s: &str) -> Result<HostPortConfigCli, String> {
2520 let (host_part, guest_part) = s.split_once('-').ok_or_else(|| {
2523 format!(
2524 "invalid hostfwd format '{s}', \
2525 expected 'proto:[hostaddr]:hostport-[guestaddr]:guestport'"
2526 )
2527 })?;
2528
2529 let (proto, host_addr_port) = host_part.split_once(':').ok_or_else(|| {
2531 format!("invalid hostfwd host part '{host_part}', expected 'proto:[hostaddr]:hostport'")
2532 })?;
2533 let protocol = match proto {
2534 "tcp" => HostPortProtocolCli::Tcp,
2535 "udp" => HostPortProtocolCli::Udp,
2536 other => {
2537 return Err(format!(
2538 "unknown hostfwd protocol '{other}', expected 'tcp' or 'udp'"
2539 ));
2540 }
2541 };
2542
2543 let (host_address, host_port) = parse_addr_port(host_addr_port)
2544 .map_err(|e| format!("invalid hostfwd host address/port: {e}"))?;
2545 let (_, guest_port) = parse_addr_port(guest_part)
2546 .map_err(|e| format!("invalid hostfwd guest address/port: {e}"))?;
2547
2548 Ok(HostPortConfigCli {
2549 protocol,
2550 host_address,
2551 host_port,
2552 guest_port,
2553 })
2554}
2555
2556fn parse_addr_port(s: &str) -> Result<(Option<std::net::IpAddr>, u16), String> {
2562 if let Some(rest) = s.strip_prefix('[') {
2563 let (addr, port) = rest
2565 .split_once("]:")
2566 .ok_or_else(|| format!("expected '[addr]:port', got '[{rest}'"))?;
2567 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
2568 let addr: std::net::IpAddr = addr
2569 .parse()
2570 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
2571 Ok((Some(addr), port))
2572 } else {
2573 match s.rsplit_once(':') {
2574 Some((addr, port)) => {
2575 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
2576 let addr = if addr.is_empty() {
2577 None
2578 } else {
2579 let parsed: std::net::IpAddr = addr
2580 .parse()
2581 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
2582 Some(parsed)
2583 };
2584 Ok((addr, port))
2585 }
2586 None => {
2587 let port: u16 = s.parse().map_err(|_| format!("invalid port '{s}'"))?;
2588 Ok((None, port))
2589 }
2590 }
2591 }
2592}
2593
2594impl FromStr for EndpointConfigCli {
2595 type Err = String;
2596
2597 fn from_str(s: &str) -> Result<Self, Self::Err> {
2598 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
2599 ["none"] => EndpointConfigCli::None,
2600 ["consomme", rest @ ..] => {
2601 let remaining = rest.join(":");
2602 let mut cidr = None;
2603 let mut host_fwd = Vec::new();
2604 for opt in remaining.split(',').filter(|s| !s.is_empty()) {
2605 if let Some(fwd) = opt.strip_prefix("hostfwd=") {
2606 host_fwd.push(parse_hostfwd(fwd)?);
2607 } else if cidr.is_none() {
2608 cidr = Some(opt.to_owned());
2609 } else {
2610 return Err(format!("unexpected consomme option '{opt}'"));
2611 }
2612 }
2613 EndpointConfigCli::Consomme { cidr, host_fwd }
2614 }
2615 ["dio", s @ ..] => EndpointConfigCli::Dio {
2616 id: s.first().map(|s| (*s).to_owned()),
2617 },
2618 ["tap", name] => EndpointConfigCli::Tap {
2619 name: (*name).to_owned(),
2620 },
2621 _ => return Err("invalid network backend".into()),
2622 };
2623
2624 Ok(ret)
2625 }
2626}
2627
2628#[derive(Clone, Debug, PartialEq)]
2629pub struct NicConfigCli {
2630 pub vtl: DeviceVtl,
2631 pub endpoint: EndpointConfigCli,
2632 pub max_queues: Option<u16>,
2633 pub underhill: bool,
2634 pub pcie_port: Option<String>,
2635}
2636
2637impl FromStr for NicConfigCli {
2638 type Err = String;
2639
2640 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
2641 let mut vtl = DeviceVtl::Vtl0;
2642 let mut max_queues = None;
2643 let mut underhill = false;
2644 let mut pcie_port = None;
2645 while let Some((opt, rest)) = s.split_once(':') {
2646 if let Some((opt, val)) = opt.split_once('=') {
2647 match opt {
2648 "queues" => {
2649 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
2650 }
2651 "pcie_port" => {
2652 if val.is_empty() {
2653 return Err("`pcie_port=` requires port name argument".into());
2654 }
2655 pcie_port = Some(val.to_string());
2656 }
2657 _ => break,
2658 }
2659 } else {
2660 match opt {
2661 "vtl2" => {
2662 vtl = DeviceVtl::Vtl2;
2663 }
2664 "uh" => underhill = true,
2665 _ => break,
2666 }
2667 }
2668 s = rest;
2669 }
2670
2671 if underhill && vtl != DeviceVtl::Vtl0 {
2672 return Err("`uh` is incompatible with `vtl2`".into());
2673 }
2674
2675 if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
2676 return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
2677 }
2678
2679 let endpoint = s.parse()?;
2680 Ok(NicConfigCli {
2681 vtl,
2682 endpoint,
2683 max_queues,
2684 underhill,
2685 pcie_port,
2686 })
2687 }
2688}
2689
2690#[derive(Debug, Error)]
2691#[error("unknown VTL2 relocation type: {0}")]
2692pub struct UnknownVtl2RelocationType(String);
2693
2694fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
2695 match s {
2696 "disable" => Ok(Vtl2BaseAddressType::File),
2697 s if s.starts_with("auto=") => {
2698 let s = s.strip_prefix("auto=").unwrap_or_default();
2699 let size = if s == "filesize" {
2700 None
2701 } else {
2702 let size = parse_memory(s).map_err(|e| {
2703 UnknownVtl2RelocationType(format!(
2704 "unable to parse memory size from {} for 'auto=' type, {e}",
2705 e
2706 ))
2707 })?;
2708 Some(size)
2709 };
2710 Ok(Vtl2BaseAddressType::MemoryLayout { size })
2711 }
2712 s if s.starts_with("absolute=") => {
2713 let s = s.strip_prefix("absolute=");
2714 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
2715 UnknownVtl2RelocationType(format!(
2716 "unable to parse number from {} for 'absolute=' type",
2717 e
2718 ))
2719 })?;
2720 Ok(Vtl2BaseAddressType::Absolute(addr))
2721 }
2722 s if s.starts_with("vtl2=") => {
2723 let s = s.strip_prefix("vtl2=").unwrap_or_default();
2724 let size = if s == "filesize" {
2725 None
2726 } else {
2727 let size = parse_memory(s).map_err(|e| {
2728 UnknownVtl2RelocationType(format!(
2729 "unable to parse memory size from {} for 'vtl2=' type, {e}",
2730 e
2731 ))
2732 })?;
2733 Some(size)
2734 };
2735 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
2736 }
2737 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
2738 }
2739}
2740
2741#[derive(Debug, Copy, Clone, PartialEq)]
2742pub enum SmtConfigCli {
2743 Auto,
2744 Force,
2745 Off,
2746}
2747
2748#[derive(Debug, Error)]
2749#[error("expected auto, force, or off")]
2750pub struct BadSmtConfig;
2751
2752impl FromStr for SmtConfigCli {
2753 type Err = BadSmtConfig;
2754
2755 fn from_str(s: &str) -> Result<Self, Self::Err> {
2756 let r = match s {
2757 "auto" => Self::Auto,
2758 "force" => Self::Force,
2759 "off" => Self::Off,
2760 _ => return Err(BadSmtConfig),
2761 };
2762 Ok(r)
2763 }
2764}
2765
2766#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
2767fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
2768 let r = match s {
2769 "auto" => X2ApicConfig::Auto,
2770 "supported" => X2ApicConfig::Supported,
2771 "off" => X2ApicConfig::Unsupported,
2772 "on" => X2ApicConfig::Enabled,
2773 _ => return Err("expected auto, supported, off, or on"),
2774 };
2775 Ok(r)
2776}
2777
2778#[derive(Debug, Copy, Clone, ValueEnum)]
2779pub enum Vtl0LateMapPolicyCli {
2780 Off,
2781 Log,
2782 Halt,
2783 Exception,
2784}
2785
2786#[derive(Debug, Copy, Clone, Default, ValueEnum)]
2788pub enum GicMsiCli {
2789 #[default]
2791 Auto,
2792 Its,
2794 V2m,
2796}
2797
2798#[derive(Debug, Copy, Clone, ValueEnum)]
2799pub enum IsolationCli {
2800 Vbs,
2801}
2802
2803#[derive(Debug, Copy, Clone, PartialEq)]
2804pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
2805
2806impl FromStr for PcatBootOrderCli {
2807 type Err = &'static str;
2808
2809 fn from_str(s: &str) -> Result<Self, Self::Err> {
2810 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
2811 let mut order = Vec::new();
2812
2813 for item in s.split(',') {
2814 let device = match item {
2815 "optical" => PcatBootDevice::Optical,
2816 "hdd" => PcatBootDevice::HardDrive,
2817 "net" => PcatBootDevice::Network,
2818 "floppy" => PcatBootDevice::Floppy,
2819 _ => return Err("unknown boot device type"),
2820 };
2821
2822 let default_pos = default_order
2823 .iter()
2824 .position(|x| x == &Some(device))
2825 .ok_or("cannot pass duplicate boot devices")?;
2826
2827 order.push(default_order[default_pos].take().unwrap());
2828 }
2829
2830 order.extend(default_order.into_iter().flatten());
2831 assert_eq!(order.len(), 4);
2832
2833 Ok(Self(order.try_into().unwrap()))
2834 }
2835}
2836
2837#[derive(Copy, Clone, Debug, ValueEnum)]
2838pub enum UefiConsoleModeCli {
2839 Default,
2840 Com1,
2841 Com2,
2842 None,
2843}
2844
2845#[derive(Copy, Clone, Debug, Default, ValueEnum)]
2846pub enum EfiDiagnosticsLogLevelCli {
2847 #[default]
2848 Default,
2849 Info,
2850 Full,
2851}
2852
2853#[derive(Clone, Debug, PartialEq)]
2854pub struct PcieRootComplexCli {
2855 pub name: String,
2856 pub segment: u16,
2857 pub start_bus: u8,
2858 pub end_bus: u8,
2859 pub low_mmio: u32,
2860 pub high_mmio: u64,
2861 pub hdm: u64,
2862 pub hdm_window_restrictions: CfmwsWindowRestrictions,
2863}
2864
2865impl FromStr for PcieRootComplexCli {
2866 type Err = anyhow::Error;
2867
2868 fn from_str(s: &str) -> Result<Self, Self::Err> {
2869 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 =
2873 CfmwsWindowRestrictions::DEVICE_COHERENT;
2874
2875 let mut opts = s.split(',');
2876 let name = opts.next().context("expected root complex name")?;
2877 if name.is_empty() {
2878 anyhow::bail!("must provide a root complex name");
2879 }
2880
2881 let mut segment = 0;
2882 let mut start_bus = 0;
2883 let mut end_bus = 255;
2884 let mut low_mmio = DEFAULT_PCIE_CRS_LOW_SIZE;
2885 let mut high_mmio = DEFAULT_PCIE_CRS_HIGH_SIZE;
2886 let mut hdm = DEFAULT_PCIE_HDM_SIZE;
2887 let mut hdm_window_restrictions = DEFAULT_HDM_WINDOW_RESTRICTIONS;
2888 for opt in opts {
2889 let mut s = opt.split('=');
2890 let opt = s.next().context("expected option")?;
2891 match opt {
2892 "segment" => {
2893 let seg_str = s.next().context("expected segment number")?;
2894 segment = u16::from_str(seg_str).context("failed to parse segment number")?;
2895 }
2896 "start_bus" => {
2897 let bus_str = s.next().context("expected start bus number")?;
2898 start_bus =
2899 u8::from_str(bus_str).context("failed to parse start bus number")?;
2900 }
2901 "end_bus" => {
2902 let bus_str = s.next().context("expected end bus number")?;
2903 end_bus = u8::from_str(bus_str).context("failed to parse end bus number")?;
2904 }
2905 "low_mmio" => {
2906 let low_mmio_str = s.next().context("expected low MMIO size")?;
2907 low_mmio = parse_memory(low_mmio_str)
2908 .context("failed to parse low MMIO size")?
2909 .try_into()?;
2910 }
2911 "high_mmio" => {
2912 let high_mmio_str = s.next().context("expected high MMIO size")?;
2913 high_mmio =
2914 parse_memory(high_mmio_str).context("failed to parse high MMIO size")?;
2915 }
2916 "hdm" => {
2917 let hdm_str = s.next().context("expected HDM decoder size")?;
2918 hdm = parse_memory(hdm_str).context("failed to parse HDM decoder size")?;
2919 }
2920 "hdm_window_restrictions" => {
2921 let mask_str = s
2922 .next()
2923 .context("expected HDM window restrictions bitmask")?;
2924 hdm_window_restrictions =
2925 parse_cxl_cfmws_window_restriction_u16_bitmask(mask_str)
2926 .context("failed to parse HDM window restrictions bitmask")?;
2927 }
2928 opt => anyhow::bail!("unknown option: '{opt}'"),
2929 }
2930 }
2931
2932 if start_bus >= end_bus {
2933 anyhow::bail!("start_bus must be less than or equal to end_bus");
2934 }
2935
2936 Ok(PcieRootComplexCli {
2937 name: name.to_string(),
2938 segment,
2939 start_bus,
2940 end_bus,
2941 low_mmio,
2942 high_mmio,
2943 hdm,
2944 hdm_window_restrictions,
2945 })
2946 }
2947}
2948
2949fn parse_cxl_cfmws_window_restriction_u16_bitmask(
2950 s: &str,
2951) -> anyhow::Result<CfmwsWindowRestrictions> {
2952 let bits = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
2953 u16::from_str_radix(hex, 16).context("invalid hex bitmask")?
2954 } else {
2955 u16::from_str(s).context("invalid decimal bitmask")?
2956 };
2957
2958 CfmwsWindowRestrictions::try_from_bits(bits)
2959 .context("bitmask includes reserved CFMWS window restriction bits")
2960}
2961
2962#[derive(Clone, Debug, PartialEq)]
2963pub struct PcieRootPortCli {
2964 pub root_complex_name: String,
2965 pub name: String,
2966 pub hotplug: bool,
2967 pub acs_capabilities_supported: Option<u16>,
2968 pub cxl: bool,
2969}
2970
2971impl FromStr for PcieRootPortCli {
2972 type Err = anyhow::Error;
2973
2974 fn from_str(s: &str) -> Result<Self, Self::Err> {
2975 let mut opts = s.split(',');
2976 let names = opts.next().context("expected root port identifiers")?;
2977 if names.is_empty() {
2978 anyhow::bail!("must provide root port identifiers");
2979 }
2980
2981 let mut s = names.split(':');
2982 let rc_name = s.next().context("expected name of parent root complex")?;
2983 let rp_name = s.next().context("expected root port name")?;
2984
2985 if let Some(extra) = s.next() {
2986 anyhow::bail!("unexpected token: '{extra}'")
2987 }
2988
2989 let mut hotplug = false;
2990 let mut acs_capabilities_supported = None;
2991 let mut cxl = false;
2992
2993 for opt in opts {
2995 let mut kv = opt.split('=');
2996 let key = kv.next().context("expected option name")?;
2997 let value = kv.next();
2998
2999 match key {
3000 "hotplug" => {
3001 if value.is_some() {
3002 anyhow::bail!("hotplug option does not take a value")
3003 }
3004 hotplug = true;
3005 }
3006 "acs" => {
3007 let value = value.context("acs option requires a value")?;
3008 if kv.next().is_some() {
3009 anyhow::bail!("acs option expects a single value")
3010 }
3011 acs_capabilities_supported = Some(parse_acs_capability_mask(value)?);
3012 }
3013 "cxl" => {
3014 if value.is_some() {
3015 anyhow::bail!("cxl option does not take a value")
3016 }
3017 cxl = true;
3018 }
3019 _ => anyhow::bail!("unexpected option: '{opt}'"),
3020 }
3021 }
3022
3023 Ok(PcieRootPortCli {
3024 root_complex_name: rc_name.to_string(),
3025 name: rp_name.to_string(),
3026 hotplug,
3027 acs_capabilities_supported,
3028 cxl,
3029 })
3030 }
3031}
3032
3033#[derive(Clone, Debug, PartialEq)]
3034pub struct GenericPcieSwitchCli {
3035 pub port_name: String,
3036 pub name: String,
3037 pub num_downstream_ports: u8,
3038 pub hotplug: bool,
3039 pub acs_capabilities_supported: Option<u16>,
3040}
3041
3042impl FromStr for GenericPcieSwitchCli {
3043 type Err = anyhow::Error;
3044
3045 fn from_str(s: &str) -> Result<Self, Self::Err> {
3046 let mut opts = s.split(',');
3047 let names = opts.next().context("expected switch identifiers")?;
3048 if names.is_empty() {
3049 anyhow::bail!("must provide switch identifiers");
3050 }
3051
3052 let mut s = names.split(':');
3053 let port_name = s.next().context("expected name of parent port")?;
3054 let switch_name = s.next().context("expected switch name")?;
3055
3056 if let Some(extra) = s.next() {
3057 anyhow::bail!("unexpected token: '{extra}'")
3058 }
3059
3060 let mut num_downstream_ports = 4u8; let mut hotplug = false;
3062 let mut acs_capabilities_supported = None;
3063
3064 for opt in opts {
3065 let mut kv = opt.split('=');
3066 let key = kv.next().context("expected option name")?;
3067
3068 match key {
3069 "num_downstream_ports" => {
3070 let value = kv.next().context("expected option value")?;
3071 if let Some(extra) = kv.next() {
3072 anyhow::bail!("unexpected token: '{extra}'")
3073 }
3074 num_downstream_ports = value.parse().context("invalid num_downstream_ports")?;
3075 }
3076 "hotplug" => {
3077 if kv.next().is_some() {
3078 anyhow::bail!("hotplug option does not take a value")
3079 }
3080 hotplug = true;
3081 }
3082 "acs" => {
3083 let value = kv.next().context("acs option requires a value")?;
3084 if kv.next().is_some() {
3085 anyhow::bail!("acs option expects a single value")
3086 }
3087 acs_capabilities_supported = Some(parse_acs_capability_mask(value)?);
3088 }
3089 _ => anyhow::bail!("unknown option: '{key}'"),
3090 }
3091 }
3092
3093 Ok(GenericPcieSwitchCli {
3094 port_name: port_name.to_string(),
3095 name: switch_name.to_string(),
3096 num_downstream_ports,
3097 hotplug,
3098 acs_capabilities_supported,
3099 })
3100 }
3101}
3102
3103#[derive(Clone, Debug, PartialEq)]
3105pub struct PcieRemoteCli {
3106 pub port_name: String,
3108 pub socket_addr: Option<String>,
3110 pub hu: u16,
3112 pub controller: u16,
3114}
3115
3116impl FromStr for PcieRemoteCli {
3117 type Err = anyhow::Error;
3118
3119 fn from_str(s: &str) -> Result<Self, Self::Err> {
3120 let mut opts = s.split(',');
3121 let port_name = opts.next().context("expected port name")?;
3122 if port_name.is_empty() {
3123 anyhow::bail!("must provide a port name");
3124 }
3125
3126 let mut socket_addr = None;
3127 let mut hu = 0u16;
3128 let mut controller = 0u16;
3129
3130 for opt in opts {
3131 let mut kv = opt.split('=');
3132 let key = kv.next().context("expected option name")?;
3133 let value = kv.next();
3134
3135 match key {
3136 "socket" => {
3137 let addr = value.context("socket requires an address")?;
3138 if let Some(extra) = kv.next() {
3139 anyhow::bail!("unexpected token: '{extra}'")
3140 }
3141 if addr.is_empty() {
3142 anyhow::bail!("socket address cannot be empty");
3143 }
3144 socket_addr = Some(addr.to_string());
3145 }
3146 "hu" => {
3147 let val = value.context("hu requires a value")?;
3148 if let Some(extra) = kv.next() {
3149 anyhow::bail!("unexpected token: '{extra}'")
3150 }
3151 hu = val.parse().context("failed to parse hu")?;
3152 }
3153 "controller" => {
3154 let val = value.context("controller requires a value")?;
3155 if let Some(extra) = kv.next() {
3156 anyhow::bail!("unexpected token: '{extra}'")
3157 }
3158 controller = val.parse().context("failed to parse controller")?;
3159 }
3160 _ => anyhow::bail!("unknown option: '{key}'"),
3161 }
3162 }
3163
3164 Ok(PcieRemoteCli {
3165 port_name: port_name.to_string(),
3166 socket_addr,
3167 hu,
3168 controller,
3169 })
3170 }
3171}
3172
3173#[cfg(target_os = "linux")]
3177#[derive(Clone, Debug)]
3178pub struct VfioDeviceCli {
3179 pub port_name: String,
3181 pub pci_id: String,
3183 pub iommu: Option<String>,
3186}
3187
3188#[cfg(target_os = "linux")]
3189impl FromStr for VfioDeviceCli {
3190 type Err = anyhow::Error;
3191
3192 fn from_str(s: &str) -> Result<Self, Self::Err> {
3193 let mut host: Option<String> = None;
3194 let mut port: Option<String> = None;
3195 let mut iommu: Option<String> = None;
3196
3197 for kv in s.split(',') {
3198 let (key, value) = kv
3199 .split_once('=')
3200 .context("expected key=value pair (e.g., host=0000:01:00.0,port=rp0)")?;
3201 if value.is_empty() {
3202 anyhow::bail!("--vfio: '{key}=' value cannot be empty");
3203 }
3204 match key {
3205 "host" => {
3206 if host.is_some() {
3207 anyhow::bail!("duplicate --vfio key: 'host'");
3208 }
3209 host = Some(value.to_string());
3210 }
3211 "port" => {
3212 if port.is_some() {
3213 anyhow::bail!("duplicate --vfio key: 'port'");
3214 }
3215 port = Some(value.to_string());
3216 }
3217 "iommu" => {
3218 if iommu.is_some() {
3219 anyhow::bail!("duplicate --vfio key: 'iommu'");
3220 }
3221 iommu = Some(value.to_string());
3222 }
3223 _ => anyhow::bail!("unknown --vfio key: '{key}'"),
3224 }
3225 }
3226
3227 let pci_id = host.context("--vfio: 'host=' is required")?;
3228 let port_name = port.context("--vfio: 'port=' is required")?;
3229
3230 if pci_id.contains('/') || pci_id.contains("..") {
3232 anyhow::bail!("PCI address must not contain path separators");
3233 }
3234
3235 Ok(VfioDeviceCli {
3236 port_name,
3237 pci_id,
3238 iommu,
3239 })
3240 }
3241}
3242
3243#[cfg(target_os = "linux")]
3247#[derive(Clone, Debug)]
3248pub struct IommuCli {
3249 pub id: String,
3251}
3252
3253#[cfg(target_os = "linux")]
3254impl FromStr for IommuCli {
3255 type Err = anyhow::Error;
3256
3257 fn from_str(s: &str) -> Result<Self, Self::Err> {
3258 let (key, value) = s
3259 .split_once('=')
3260 .context("expected id=<name> (e.g., id=iommu0)")?;
3261 if key != "id" {
3262 anyhow::bail!("expected 'id=<name>', got '{key}=...'");
3263 }
3264 if value.is_empty() {
3265 anyhow::bail!("iommu id cannot be empty");
3266 }
3267 Ok(IommuCli {
3268 id: value.to_string(),
3269 })
3270 }
3271}
3272
3273fn default_value_from_arch_env(name: &str) -> OsString {
3281 let prefix = if cfg!(guest_arch = "x86_64") {
3282 "X86_64"
3283 } else if cfg!(guest_arch = "aarch64") {
3284 "AARCH64"
3285 } else {
3286 return Default::default();
3287 };
3288 let prefixed = format!("{}_{}", prefix, name);
3289 std::env::var_os(name)
3290 .or_else(|| std::env::var_os(prefixed))
3291 .unwrap_or_default()
3292}
3293
3294#[derive(Clone)]
3296pub struct OptionalPathBuf(pub Option<PathBuf>);
3297
3298impl From<&std::ffi::OsStr> for OptionalPathBuf {
3299 fn from(s: &std::ffi::OsStr) -> Self {
3300 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
3301 }
3302}
3303
3304#[cfg(target_os = "linux")]
3305#[derive(Clone)]
3306pub enum VhostUserDeviceTypeCli {
3307 Blk {
3310 num_queues: Option<u16>,
3311 queue_size: Option<u16>,
3312 },
3313 Fs {
3315 tag: String,
3316 num_queues: Option<u16>,
3317 queue_size: Option<u16>,
3318 },
3319 Other {
3321 device_id: u16,
3322 queue_sizes: Vec<u16>,
3323 },
3324}
3325
3326#[cfg(target_os = "linux")]
3327#[derive(Clone)]
3328pub struct VhostUserCli {
3329 pub socket_path: String,
3330 pub device_type: VhostUserDeviceTypeCli,
3331 pub pcie_port: Option<String>,
3332}
3333
3334#[cfg(target_os = "linux")]
3338fn split_respecting_brackets(s: &str) -> anyhow::Result<Vec<&str>> {
3339 let mut result = Vec::new();
3340 let mut start = 0;
3341 let mut depth: i32 = 0;
3342 for (i, c) in s.char_indices() {
3343 match c {
3344 '[' => depth += 1,
3345 ']' => {
3346 depth -= 1;
3347 anyhow::ensure!(depth >= 0, "unmatched ']' in option string");
3348 }
3349 ',' if depth == 0 => {
3350 result.push(&s[start..i]);
3351 start = i + 1;
3352 }
3353 _ => {}
3354 }
3355 }
3356 anyhow::ensure!(depth == 0, "unclosed '[' in option string");
3357 result.push(&s[start..]);
3358 Ok(result)
3359}
3360
3361#[cfg(target_os = "linux")]
3362impl FromStr for VhostUserCli {
3363 type Err = anyhow::Error;
3364
3365 fn from_str(s: &str) -> anyhow::Result<Self> {
3366 let parts = split_respecting_brackets(s)?;
3368 let mut parts_iter = parts.into_iter();
3369 let socket_path = parts_iter
3370 .next()
3371 .context("missing socket path")?
3372 .to_string();
3373
3374 let mut device_id: Option<u16> = None;
3375 let mut tag: Option<String> = None;
3376 let mut pcie_port: Option<String> = None;
3377 let mut type_name = None;
3378 let mut num_queues: Option<u16> = None;
3379 let mut queue_size: Option<u16> = None;
3380 let mut queue_sizes: Option<Vec<u16>> = None;
3381 for opt in parts_iter {
3382 let (key, val) = opt.split_once('=').context("expected key=value option")?;
3383 match key {
3384 "type" => {
3385 type_name = Some(val);
3386 }
3387 "device_id" => {
3388 device_id = Some(val.parse().context("invalid device_id")?);
3389 }
3390 "tag" => {
3391 tag = Some(val.to_string());
3392 }
3393 "pcie_port" => {
3394 pcie_port = Some(val.to_string());
3395 }
3396 "num_queues" => {
3397 num_queues = Some(val.parse().context("invalid num_queues")?);
3398 }
3399 "queue_size" => {
3400 queue_size = Some(val.parse().context("invalid queue_size")?);
3401 }
3402 "queue_sizes" => {
3403 let trimmed = val
3405 .strip_prefix('[')
3406 .and_then(|v| v.strip_suffix(']'))
3407 .context("queue_sizes must be bracketed: [N,N,N]")?;
3408 let sizes: Vec<u16> = trimmed
3409 .split(',')
3410 .map(|s| s.parse().context("invalid queue size in queue_sizes"))
3411 .collect::<anyhow::Result<_>>()?;
3412 anyhow::ensure!(!sizes.is_empty(), "queue_sizes must be non-empty");
3413 queue_sizes = Some(sizes);
3414 }
3415 other => anyhow::bail!("unknown vhost-user option: '{other}'"),
3416 }
3417 }
3418
3419 if type_name.is_some() == device_id.is_some() {
3420 anyhow::bail!("must specify type=<name> or device_id=<N>");
3421 }
3422
3423 let device_type = match type_name {
3425 Some("fs") => {
3426 let tag = tag.take().context("type=fs requires tag=<name>")?;
3427 VhostUserDeviceTypeCli::Fs {
3428 tag,
3429 num_queues: num_queues.take(),
3430 queue_size: queue_size.take(),
3431 }
3432 }
3433 Some("blk") => VhostUserDeviceTypeCli::Blk {
3434 num_queues: num_queues.take(),
3435 queue_size: queue_size.take(),
3436 },
3437 Some(ty) => anyhow::bail!("unknown vhost-user device type: '{ty}'"),
3438 None => {
3439 let queue_sizes = queue_sizes
3440 .take()
3441 .context("device_id= requires queue_sizes=[N,N,...]")?;
3442 VhostUserDeviceTypeCli::Other {
3443 device_id: device_id.unwrap(),
3444 queue_sizes,
3445 }
3446 }
3447 };
3448
3449 if tag.is_some() {
3450 anyhow::bail!("tag= is only valid for type=fs");
3451 }
3452 if queue_sizes.is_some() {
3453 anyhow::bail!("queue_sizes= is only valid for device_id=");
3454 }
3455 if num_queues.is_some() || queue_size.is_some() {
3456 anyhow::bail!(
3457 "num_queues= and queue_size= are not valid for device_id=; use queue_sizes="
3458 );
3459 }
3460
3461 Ok(VhostUserCli {
3462 socket_path,
3463 device_type,
3464 pcie_port,
3465 })
3466 }
3467}
3468
3469#[cfg(test)]
3470mod tests {
3471 use super::*;
3472
3473 use std::path::Path;
3474
3475 #[test]
3476 fn test_parse_file_opts() {
3477 let disk = DiskCliKind::from_str("file:test.vhd;create=1G").unwrap();
3479 assert!(matches!(
3480 &disk,
3481 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
3482 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
3483 ));
3484
3485 let disk = DiskCliKind::from_str("test.vhd;create=1G").unwrap();
3487 assert!(matches!(
3488 &disk,
3489 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
3490 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
3491 ));
3492
3493 let disk = DiskCliKind::from_str("file:/dev/sdb;direct").unwrap();
3495 assert!(matches!(
3496 &disk,
3497 DiskCliKind::File { path, create_with_len: None, direct: true }
3498 if path == Path::new("/dev/sdb")
3499 ));
3500
3501 let disk = DiskCliKind::from_str("file:disk.img;direct;create=1G").unwrap();
3503 assert!(matches!(
3504 &disk,
3505 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
3506 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
3507 ));
3508
3509 let disk = DiskCliKind::from_str("file:disk.img;create=1G;direct").unwrap();
3510 assert!(matches!(
3511 &disk,
3512 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
3513 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
3514 ));
3515
3516 let disk = DiskCliKind::from_str("file:disk.img").unwrap();
3518 assert!(matches!(
3519 &disk,
3520 DiskCliKind::File { path, create_with_len: None, direct: false }
3521 if path == Path::new("disk.img")
3522 ));
3523
3524 assert!(DiskCliKind::from_str("file:disk.img;bogus").is_err());
3526
3527 assert!(DiskCliKind::from_str("sql:db.sqlite;direct").is_err());
3529 }
3530
3531 #[test]
3532 fn test_parse_memory_disk() {
3533 let s = "mem:1G";
3534 let disk = DiskCliKind::from_str(s).unwrap();
3535 match disk {
3536 DiskCliKind::Memory(size) => {
3537 assert_eq!(size, 1024 * 1024 * 1024); }
3539 _ => panic!("Expected Memory variant"),
3540 }
3541 }
3542
3543 #[test]
3544 fn test_parse_pcie_disk() {
3545 assert_eq!(
3546 DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
3547 Some("p0".to_string())
3548 );
3549 assert_eq!(
3550 DiskCli::from_str("file:path.vhdx,pcie_port=p0")
3551 .unwrap()
3552 .pcie_port,
3553 Some("p0".to_string())
3554 );
3555 assert_eq!(
3556 DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
3557 .unwrap()
3558 .pcie_port,
3559 Some("p0".to_string())
3560 );
3561
3562 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
3564
3565 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
3567 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
3568 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
3569 }
3570
3571 #[test]
3572 fn test_parse_memory_diff_disk() {
3573 let s = "memdiff:file:base.img";
3574 let disk = DiskCliKind::from_str(s).unwrap();
3575 match disk {
3576 DiskCliKind::MemoryDiff(inner) => match *inner {
3577 DiskCliKind::File {
3578 path,
3579 create_with_len,
3580 ..
3581 } => {
3582 assert_eq!(path, PathBuf::from("base.img"));
3583 assert_eq!(create_with_len, None);
3584 }
3585 _ => panic!("Expected File variant inside MemoryDiff"),
3586 },
3587 _ => panic!("Expected MemoryDiff variant"),
3588 }
3589 }
3590
3591 #[test]
3592 fn test_parse_sqlite_disk() {
3593 let s = "sql:db.sqlite;create=2G";
3594 let disk = DiskCliKind::from_str(s).unwrap();
3595 match disk {
3596 DiskCliKind::Sqlite {
3597 path,
3598 create_with_len,
3599 } => {
3600 assert_eq!(path, PathBuf::from("db.sqlite"));
3601 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
3602 }
3603 _ => panic!("Expected Sqlite variant"),
3604 }
3605
3606 let s = "sql:db.sqlite";
3608 let disk = DiskCliKind::from_str(s).unwrap();
3609 match disk {
3610 DiskCliKind::Sqlite {
3611 path,
3612 create_with_len,
3613 } => {
3614 assert_eq!(path, PathBuf::from("db.sqlite"));
3615 assert_eq!(create_with_len, None);
3616 }
3617 _ => panic!("Expected Sqlite variant"),
3618 }
3619 }
3620
3621 #[test]
3622 fn test_parse_sqlite_diff_disk() {
3623 let s = "sqldiff:diff.sqlite;create:file:base.img";
3625 let disk = DiskCliKind::from_str(s).unwrap();
3626 match disk {
3627 DiskCliKind::SqliteDiff { path, create, disk } => {
3628 assert_eq!(path, PathBuf::from("diff.sqlite"));
3629 assert!(create);
3630 match *disk {
3631 DiskCliKind::File {
3632 path,
3633 create_with_len,
3634 ..
3635 } => {
3636 assert_eq!(path, PathBuf::from("base.img"));
3637 assert_eq!(create_with_len, None);
3638 }
3639 _ => panic!("Expected File variant inside SqliteDiff"),
3640 }
3641 }
3642 _ => panic!("Expected SqliteDiff variant"),
3643 }
3644
3645 let s = "sqldiff:diff.sqlite:file:base.img";
3647 let disk = DiskCliKind::from_str(s).unwrap();
3648 match disk {
3649 DiskCliKind::SqliteDiff { path, create, disk } => {
3650 assert_eq!(path, PathBuf::from("diff.sqlite"));
3651 assert!(!create);
3652 match *disk {
3653 DiskCliKind::File {
3654 path,
3655 create_with_len,
3656 ..
3657 } => {
3658 assert_eq!(path, PathBuf::from("base.img"));
3659 assert_eq!(create_with_len, None);
3660 }
3661 _ => panic!("Expected File variant inside SqliteDiff"),
3662 }
3663 }
3664 _ => panic!("Expected SqliteDiff variant"),
3665 }
3666 }
3667
3668 #[test]
3669 fn test_parse_autocache_sqlite_disk() {
3670 let disk =
3672 DiskCliKind::parse_autocache(":file:disk.vhd", Ok("/tmp/cache".to_string())).unwrap();
3673 assert!(matches!(
3674 disk,
3675 DiskCliKind::AutoCacheSqlite {
3676 cache_path,
3677 key,
3678 disk: _disk,
3679 } if cache_path == "/tmp/cache" && key.is_none()
3680 ));
3681
3682 let disk =
3684 DiskCliKind::parse_autocache("mykey:file:disk.vhd", Ok("/tmp/cache".to_string()))
3685 .unwrap();
3686 assert!(matches!(
3687 disk,
3688 DiskCliKind::AutoCacheSqlite {
3689 cache_path,
3690 key: Some(key),
3691 disk: _disk,
3692 } if cache_path == "/tmp/cache" && key == "mykey"
3693 ));
3694
3695 assert!(
3697 DiskCliKind::parse_autocache(":file:disk.vhd", Err(std::env::VarError::NotPresent),)
3698 .is_err()
3699 );
3700 }
3701
3702 #[test]
3703 fn test_parse_disk_errors() {
3704 assert!(DiskCliKind::from_str("invalid:").is_err());
3705 assert!(DiskCliKind::from_str("memory:extra").is_err());
3706
3707 assert!(DiskCliKind::from_str("sqlite:").is_err());
3709 }
3710
3711 #[test]
3712 fn test_parse_errors() {
3713 assert!(DiskCliKind::from_str("mem:invalid").is_err());
3715
3716 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
3718
3719 assert!(
3721 DiskCliKind::parse_autocache("key:file:disk.vhd", Err(std::env::VarError::NotPresent),)
3722 .is_err()
3723 );
3724
3725 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
3727
3728 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
3730
3731 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
3733
3734 assert!(DiskCliKind::from_str("invalid:path").is_err());
3736
3737 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
3739 }
3740
3741 #[test]
3742 fn test_fs_args_from_str() {
3743 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
3744 assert_eq!(args.tag, "tag1");
3745 assert_eq!(args.path, "/path/to/fs");
3746
3747 assert!(FsArgs::from_str("tag1").is_err());
3749 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
3750 }
3751
3752 #[test]
3753 fn test_fs_args_with_options_from_str() {
3754 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
3755 assert_eq!(args.tag, "tag1");
3756 assert_eq!(args.path, "/path/to/fs");
3757 assert_eq!(args.options, "opt1;opt2");
3758
3759 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
3761 assert_eq!(args.tag, "tag1");
3762 assert_eq!(args.path, "/path/to/fs");
3763 assert_eq!(args.options, "");
3764
3765 assert!(FsArgsWithOptions::from_str("tag1").is_err());
3767 }
3768
3769 #[test]
3770 fn test_serial_config_from_str() {
3771 assert_eq!(
3772 SerialConfigCli::from_str("none").unwrap(),
3773 SerialConfigCli::None
3774 );
3775 assert_eq!(
3776 SerialConfigCli::from_str("console").unwrap(),
3777 SerialConfigCli::Console
3778 );
3779 assert_eq!(
3780 SerialConfigCli::from_str("stderr").unwrap(),
3781 SerialConfigCli::Stderr
3782 );
3783
3784 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
3786 if let SerialConfigCli::File(path) = file_config {
3787 assert_eq!(path.to_str().unwrap(), "/path/to/file");
3788 } else {
3789 panic!("Expected File variant");
3790 }
3791
3792 match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
3794 SerialConfigCli::NewConsole(None, Some(name)) => {
3795 assert_eq!(name, "MyTerm");
3796 }
3797 _ => panic!("Expected NewConsole variant with name"),
3798 }
3799
3800 match SerialConfigCli::from_str("term").unwrap() {
3802 SerialConfigCli::NewConsole(None, None) => (),
3803 _ => panic!("Expected NewConsole variant without name"),
3804 }
3805
3806 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
3808 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
3809 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
3810 assert_eq!(name, "MyTerm");
3811 }
3812 _ => panic!("Expected NewConsole variant with name"),
3813 }
3814
3815 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
3817 SerialConfigCli::NewConsole(Some(path), None) => {
3818 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
3819 }
3820 _ => panic!("Expected NewConsole variant without name"),
3821 }
3822
3823 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
3825 SerialConfigCli::Tcp(addr) => {
3826 assert_eq!(addr.to_string(), "127.0.0.1:1234");
3827 }
3828 _ => panic!("Expected Tcp variant"),
3829 }
3830
3831 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
3833 SerialConfigCli::Pipe(path) => {
3834 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
3835 }
3836 _ => panic!("Expected Pipe variant"),
3837 }
3838
3839 assert!(SerialConfigCli::from_str("").is_err());
3841 assert!(SerialConfigCli::from_str("unknown").is_err());
3842 assert!(SerialConfigCli::from_str("file").is_err());
3843 assert!(SerialConfigCli::from_str("listen").is_err());
3844 }
3845
3846 #[test]
3847 fn test_endpoint_config_from_str() {
3848 assert!(matches!(
3850 EndpointConfigCli::from_str("none").unwrap(),
3851 EndpointConfigCli::None
3852 ));
3853
3854 match EndpointConfigCli::from_str("consomme").unwrap() {
3856 EndpointConfigCli::Consomme {
3857 cidr: None,
3858 host_fwd,
3859 } => assert!(host_fwd.is_empty()),
3860 _ => panic!("Expected Consomme variant without cidr"),
3861 }
3862
3863 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
3865 EndpointConfigCli::Consomme {
3866 cidr: Some(cidr),
3867 host_fwd,
3868 } => {
3869 assert_eq!(cidr, "192.168.0.0/24");
3870 assert!(host_fwd.is_empty());
3871 }
3872 _ => panic!("Expected Consomme variant with cidr"),
3873 }
3874
3875 match EndpointConfigCli::from_str("consomme:hostfwd=udp:127.0.0.1:5000-:5000").unwrap() {
3877 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3878 assert!(cidr.is_none());
3879 assert_eq!(host_fwd.len(), 1);
3880 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Udp);
3881 assert_eq!(
3882 host_fwd[0].host_address,
3883 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
3884 );
3885 assert_eq!(host_fwd[0].host_port, 5000);
3886 assert_eq!(host_fwd[0].guest_port, 5000);
3887 }
3888 _ => panic!("Expected Consomme variant with hostfwd"),
3889 }
3890
3891 match EndpointConfigCli::from_str("consomme:10.0.0.0/24,hostfwd=tcp::2222-:22").unwrap() {
3893 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3894 assert_eq!(cidr.as_deref(), Some("10.0.0.0/24"));
3895 assert_eq!(host_fwd.len(), 1);
3896 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3897 assert_eq!(host_fwd[0].host_port, 2222);
3898 assert_eq!(host_fwd[0].guest_port, 22);
3899 }
3900 _ => panic!("Expected Consomme variant with cidr and hostfwd"),
3901 }
3902
3903 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::2222-:22,hostfwd=tcp::3389-:3389")
3905 .unwrap()
3906 {
3907 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3908 assert!(cidr.is_none());
3909 assert_eq!(host_fwd.len(), 2);
3910 assert_eq!(host_fwd[0].host_port, 2222);
3911 assert_eq!(host_fwd[0].guest_port, 22);
3912 assert_eq!(host_fwd[1].host_port, 3389);
3913 assert_eq!(host_fwd[1].guest_port, 3389);
3914 }
3915 _ => panic!("Expected Consomme variant with multiple hostfwd"),
3916 }
3917
3918 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:127.0.0.1:8080-:80").unwrap() {
3920 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3921 assert!(cidr.is_none());
3922 assert_eq!(host_fwd.len(), 1);
3923 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3924 assert_eq!(
3925 host_fwd[0].host_address,
3926 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
3927 );
3928 assert_eq!(host_fwd[0].host_port, 8080);
3929 assert_eq!(host_fwd[0].guest_port, 80);
3930 }
3931 _ => panic!("Expected Consomme variant with host/guest port mapping"),
3932 }
3933
3934 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-10.0.0.2:80").unwrap() {
3936 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3937 assert!(cidr.is_none());
3938 assert_eq!(host_fwd[0].host_port, 8080);
3939 assert_eq!(host_fwd[0].guest_port, 80);
3940 }
3941 _ => panic!("Expected Consomme variant with guest address"),
3942 }
3943
3944 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:[::1]:8080-:80").unwrap() {
3946 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3947 assert!(cidr.is_none());
3948 assert_eq!(host_fwd.len(), 1);
3949 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3950 assert_eq!(
3951 host_fwd[0].host_address,
3952 Some(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST))
3953 );
3954 assert_eq!(host_fwd[0].host_port, 8080);
3955 assert_eq!(host_fwd[0].guest_port, 80);
3956 }
3957 _ => panic!("Expected Consomme variant with IPv6 hostfwd"),
3958 }
3959
3960 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-[::1]:80").unwrap() {
3962 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3963 assert!(cidr.is_none());
3964 assert_eq!(host_fwd[0].host_port, 8080);
3965 assert_eq!(host_fwd[0].guest_port, 80);
3966 }
3967 _ => panic!("Expected Consomme variant with IPv6 guest address"),
3968 }
3969
3970 match EndpointConfigCli::from_str("dio").unwrap() {
3972 EndpointConfigCli::Dio { id: None } => (),
3973 _ => panic!("Expected Dio variant without id"),
3974 }
3975
3976 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
3978 EndpointConfigCli::Dio { id: Some(id) } => {
3979 assert_eq!(id, "test_id");
3980 }
3981 _ => panic!("Expected Dio variant with id"),
3982 }
3983
3984 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
3986 EndpointConfigCli::Tap { name } => {
3987 assert_eq!(name, "tap0");
3988 }
3989 _ => panic!("Expected Tap variant"),
3990 }
3991
3992 assert!(EndpointConfigCli::from_str("invalid").is_err());
3994 }
3995
3996 #[test]
3997 fn test_nic_config_from_str() {
3998 use openvmm_defs::config::DeviceVtl;
3999
4000 let config = NicConfigCli::from_str("none").unwrap();
4002 assert_eq!(config.vtl, DeviceVtl::Vtl0);
4003 assert!(config.max_queues.is_none());
4004 assert!(!config.underhill);
4005 assert!(config.pcie_port.is_none());
4006 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4007
4008 let config = NicConfigCli::from_str("vtl2:none").unwrap();
4010 assert_eq!(config.vtl, DeviceVtl::Vtl2);
4011 assert!(config.pcie_port.is_none());
4012 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4013
4014 let config = NicConfigCli::from_str("queues=4:none").unwrap();
4016 assert_eq!(config.max_queues, Some(4));
4017 assert!(config.pcie_port.is_none());
4018 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4019
4020 let config = NicConfigCli::from_str("uh:none").unwrap();
4022 assert!(config.underhill);
4023 assert!(config.pcie_port.is_none());
4024 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4025
4026 let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
4028 assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
4029 assert!(matches!(config.endpoint, EndpointConfigCli::None));
4030
4031 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
4033 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
4035 assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
4036 assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
4037 assert!(NicConfigCli::from_str("pcie_port:none").is_err());
4038 }
4039
4040 #[test]
4041 fn test_parse_pcie_port_prefix() {
4042 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
4044 assert_eq!(port.unwrap(), "rp0");
4045 assert_eq!(rest, "tag,path");
4046
4047 let (port, rest) = parse_pcie_port_prefix("tag,path");
4049 assert!(port.is_none());
4050 assert_eq!(rest, "tag,path");
4051
4052 let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
4054 assert!(port.is_none());
4055 assert_eq!(rest, "pcie_port=:tag,path");
4056
4057 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
4059 assert!(port.is_none());
4060 assert_eq!(rest, "pcie_port=rp0");
4061 }
4062
4063 #[test]
4064 fn test_cxl_test_device_cli_parse_valid() {
4065 let cfg = CxlTestDeviceCli::from_str("mem:1G,pcie_port=rp0").unwrap();
4066 assert_eq!(cfg.hdm_size, 1024 * 1024 * 1024);
4067 assert_eq!(cfg.pcie_port, "rp0");
4068 }
4069
4070 #[test]
4071 fn test_cxl_test_device_cli_parse_invalid() {
4072 assert!(CxlTestDeviceCli::from_str("file:disk.img,pcie_port=rp0").is_err());
4073 assert!(CxlTestDeviceCli::from_str("mem:1G").is_err());
4074 assert!(CxlTestDeviceCli::from_str("mem:1G,pcie_port=").is_err());
4075 }
4076
4077 #[test]
4078 fn test_fs_args_pcie_port() {
4079 let args = FsArgs::from_str("myfs,/path").unwrap();
4081 assert_eq!(args.tag, "myfs");
4082 assert_eq!(args.path, "/path");
4083 assert!(args.pcie_port.is_none());
4084
4085 let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
4087 assert_eq!(args.pcie_port.unwrap(), "rp0");
4088 assert_eq!(args.tag, "myfs");
4089 assert_eq!(args.path, "/path");
4090
4091 assert!(FsArgs::from_str("myfs").is_err());
4093 assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
4094 }
4095
4096 #[test]
4097 fn test_fs_args_with_options_pcie_port() {
4098 let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
4100 assert_eq!(args.tag, "myfs");
4101 assert_eq!(args.path, "/path");
4102 assert_eq!(args.options, "uid=1000");
4103 assert!(args.pcie_port.is_none());
4104
4105 let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
4107 assert_eq!(args.pcie_port.unwrap(), "rp0");
4108 assert_eq!(args.tag, "myfs");
4109 assert_eq!(args.path, "/path");
4110 assert_eq!(args.options, "uid=1000");
4111
4112 assert!(FsArgsWithOptions::from_str("myfs").is_err());
4114 }
4115
4116 #[test]
4117 fn test_virtio_pmem_args_pcie_port() {
4118 let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
4120 assert_eq!(args.path, "/path/to/file");
4121 assert!(args.pcie_port.is_none());
4122
4123 let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
4125 assert_eq!(args.pcie_port.unwrap(), "rp0");
4126 assert_eq!(args.path, "/path/to/file");
4127
4128 assert!(VirtioPmemArgs::from_str("").is_err());
4130 assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
4131 }
4132
4133 #[test]
4134 fn test_smt_config_from_str() {
4135 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
4136 assert_eq!(
4137 SmtConfigCli::from_str("force").unwrap(),
4138 SmtConfigCli::Force
4139 );
4140 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
4141
4142 assert!(SmtConfigCli::from_str("invalid").is_err());
4144 assert!(SmtConfigCli::from_str("").is_err());
4145 }
4146
4147 #[test]
4148 fn test_pcat_boot_order_from_str() {
4149 let order = PcatBootOrderCli::from_str("optical").unwrap();
4151 assert_eq!(order.0[0], PcatBootDevice::Optical);
4152
4153 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
4155 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
4156 assert_eq!(order.0[1], PcatBootDevice::Network);
4157
4158 assert!(PcatBootOrderCli::from_str("invalid").is_err());
4160 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
4162
4163 #[test]
4164 fn test_floppy_disk_from_str() {
4165 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
4167 assert!(!disk.read_only);
4168 match disk.kind {
4169 DiskCliKind::File {
4170 path,
4171 create_with_len,
4172 ..
4173 } => {
4174 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
4175 assert_eq!(create_with_len, None);
4176 }
4177 _ => panic!("Expected File variant"),
4178 }
4179
4180 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
4182 assert!(disk.read_only);
4183
4184 assert!(FloppyDiskCli::from_str("").is_err());
4186 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
4187 }
4188
4189 #[test]
4190 fn test_pcie_root_complex_from_str() {
4191 const ONE_MB: u64 = 1024 * 1024;
4192 const ONE_GB: u64 = 1024 * ONE_MB;
4193
4194 const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
4195 const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
4196 const DEFAULT_HDM: u64 = ONE_GB;
4197 const DEFAULT_HDM_WINDOW_RESTRICTIONS: CfmwsWindowRestrictions =
4198 CfmwsWindowRestrictions::DEVICE_COHERENT;
4199
4200 assert_eq!(
4201 PcieRootComplexCli::from_str("rc0").unwrap(),
4202 PcieRootComplexCli {
4203 name: "rc0".to_string(),
4204 segment: 0,
4205 start_bus: 0,
4206 end_bus: 255,
4207 low_mmio: DEFAULT_LOW_MMIO,
4208 high_mmio: DEFAULT_HIGH_MMIO,
4209 hdm: DEFAULT_HDM,
4210 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4211 }
4212 );
4213
4214 assert_eq!(
4215 PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
4216 PcieRootComplexCli {
4217 name: "rc1".to_string(),
4218 segment: 1,
4219 start_bus: 0,
4220 end_bus: 255,
4221 low_mmio: DEFAULT_LOW_MMIO,
4222 high_mmio: DEFAULT_HIGH_MMIO,
4223 hdm: DEFAULT_HDM,
4224 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4225 }
4226 );
4227
4228 assert_eq!(
4229 PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
4230 PcieRootComplexCli {
4231 name: "rc2".to_string(),
4232 segment: 0,
4233 start_bus: 32,
4234 end_bus: 255,
4235 low_mmio: DEFAULT_LOW_MMIO,
4236 high_mmio: DEFAULT_HIGH_MMIO,
4237 hdm: DEFAULT_HDM,
4238 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4239 }
4240 );
4241
4242 assert_eq!(
4243 PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
4244 PcieRootComplexCli {
4245 name: "rc3".to_string(),
4246 segment: 0,
4247 start_bus: 0,
4248 end_bus: 31,
4249 low_mmio: DEFAULT_LOW_MMIO,
4250 high_mmio: DEFAULT_HIGH_MMIO,
4251 hdm: DEFAULT_HDM,
4252 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4253 }
4254 );
4255
4256 assert_eq!(
4257 PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
4258 PcieRootComplexCli {
4259 name: "rc4".to_string(),
4260 segment: 0,
4261 start_bus: 32,
4262 end_bus: 127,
4263 low_mmio: DEFAULT_LOW_MMIO,
4264 high_mmio: 2 * ONE_GB,
4265 hdm: DEFAULT_HDM,
4266 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4267 }
4268 );
4269
4270 assert_eq!(
4271 PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
4272 PcieRootComplexCli {
4273 name: "rc5".to_string(),
4274 segment: 2,
4275 start_bus: 32,
4276 end_bus: 127,
4277 low_mmio: DEFAULT_LOW_MMIO,
4278 high_mmio: DEFAULT_HIGH_MMIO,
4279 hdm: DEFAULT_HDM,
4280 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4281 }
4282 );
4283
4284 assert_eq!(
4285 PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
4286 PcieRootComplexCli {
4287 name: "rc6".to_string(),
4288 segment: 0,
4289 start_bus: 0,
4290 end_bus: 255,
4291 low_mmio: ONE_MB as u32,
4292 high_mmio: 64 * ONE_GB,
4293 hdm: DEFAULT_HDM,
4294 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4295 }
4296 );
4297
4298 assert_eq!(
4299 PcieRootComplexCli::from_str("rc7,hdm=2G").unwrap(),
4300 PcieRootComplexCli {
4301 name: "rc7".to_string(),
4302 segment: 0,
4303 start_bus: 0,
4304 end_bus: 255,
4305 low_mmio: DEFAULT_LOW_MMIO,
4306 high_mmio: DEFAULT_HIGH_MMIO,
4307 hdm: 2 * ONE_GB,
4308 hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4309 }
4310 );
4311
4312 assert_eq!(
4313 PcieRootComplexCli::from_str("rc8,hdm_window_restrictions=0x21").unwrap(),
4314 PcieRootComplexCli {
4315 name: "rc8".to_string(),
4316 segment: 0,
4317 start_bus: 0,
4318 end_bus: 255,
4319 low_mmio: DEFAULT_LOW_MMIO,
4320 high_mmio: DEFAULT_HIGH_MMIO,
4321 hdm: DEFAULT_HDM,
4322 hdm_window_restrictions: CfmwsWindowRestrictions::try_from_bits(0x21).unwrap(),
4323 }
4324 );
4325
4326 assert!(PcieRootComplexCli::from_str("").is_err());
4328 assert!(PcieRootComplexCli::from_str("poorly,").is_err());
4329 assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
4330 assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
4331 assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
4332 assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
4333 assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
4334 assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
4335 assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
4336 assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
4337 assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
4338 assert!(PcieRootComplexCli::from_str("rc,hdm=bad").is_err());
4339 assert!(PcieRootComplexCli::from_str("rc,hdm").is_err());
4340 assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions=bad").is_err());
4341 assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions").is_err());
4342 assert!(PcieRootComplexCli::from_str("rc,cxl").is_err());
4343 }
4344
4345 #[test]
4346 fn test_pcie_root_port_from_str() {
4347 assert_eq!(
4348 PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
4349 PcieRootPortCli {
4350 root_complex_name: "rc0".to_string(),
4351 name: "rc0rp0".to_string(),
4352 hotplug: false,
4353 acs_capabilities_supported: None,
4354 cxl: false,
4355 }
4356 );
4357
4358 assert_eq!(
4359 PcieRootPortCli::from_str("my_rc:port2").unwrap(),
4360 PcieRootPortCli {
4361 root_complex_name: "my_rc".to_string(),
4362 name: "port2".to_string(),
4363 hotplug: false,
4364 acs_capabilities_supported: None,
4365 cxl: false,
4366 }
4367 );
4368
4369 assert_eq!(
4371 PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
4372 PcieRootPortCli {
4373 root_complex_name: "my_rc".to_string(),
4374 name: "port2".to_string(),
4375 hotplug: true,
4376 acs_capabilities_supported: None,
4377 cxl: false,
4378 }
4379 );
4380
4381 assert_eq!(
4382 PcieRootPortCli::from_str("my_rc:port3,acs=0").unwrap(),
4383 PcieRootPortCli {
4384 root_complex_name: "my_rc".to_string(),
4385 name: "port3".to_string(),
4386 hotplug: false,
4387 acs_capabilities_supported: Some(0),
4388 cxl: false,
4389 }
4390 );
4391
4392 assert_eq!(
4393 PcieRootPortCli::from_str("my_rc:port3,acs=0x5f").unwrap(),
4394 PcieRootPortCli {
4395 root_complex_name: "my_rc".to_string(),
4396 name: "port3".to_string(),
4397 hotplug: false,
4398 acs_capabilities_supported: Some(0x005f),
4399 cxl: false,
4400 }
4401 );
4402
4403 assert_eq!(
4404 PcieRootPortCli::from_str("my_rc:port4,cxl").unwrap(),
4405 PcieRootPortCli {
4406 root_complex_name: "my_rc".to_string(),
4407 name: "port4".to_string(),
4408 hotplug: false,
4409 acs_capabilities_supported: None,
4410 cxl: true,
4411 }
4412 );
4413
4414 assert!(PcieRootPortCli::from_str("").is_err());
4416 assert!(PcieRootPortCli::from_str("rp0").is_err());
4417 assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
4418 assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
4419 assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
4420 assert!(PcieRootPortCli::from_str("rc0:rp0,cxl=true").is_err());
4421 }
4422
4423 #[test]
4424 fn test_pcie_switch_from_str() {
4425 assert_eq!(
4426 GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
4427 GenericPcieSwitchCli {
4428 port_name: "rp0".to_string(),
4429 name: "switch0".to_string(),
4430 num_downstream_ports: 4,
4431 hotplug: false,
4432 acs_capabilities_supported: None,
4433 }
4434 );
4435
4436 assert_eq!(
4437 GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
4438 GenericPcieSwitchCli {
4439 port_name: "port1".to_string(),
4440 name: "my_switch".to_string(),
4441 num_downstream_ports: 4,
4442 hotplug: false,
4443 acs_capabilities_supported: None,
4444 }
4445 );
4446
4447 assert_eq!(
4448 GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
4449 GenericPcieSwitchCli {
4450 port_name: "rp2".to_string(),
4451 name: "sw".to_string(),
4452 num_downstream_ports: 8,
4453 hotplug: false,
4454 acs_capabilities_supported: None,
4455 }
4456 );
4457
4458 assert_eq!(
4460 GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
4461 GenericPcieSwitchCli {
4462 port_name: "switch0-downstream-1".to_string(),
4463 name: "child_switch".to_string(),
4464 num_downstream_ports: 4,
4465 hotplug: false,
4466 acs_capabilities_supported: None,
4467 }
4468 );
4469
4470 assert_eq!(
4472 GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
4473 GenericPcieSwitchCli {
4474 port_name: "rp0".to_string(),
4475 name: "switch0".to_string(),
4476 num_downstream_ports: 4,
4477 hotplug: true,
4478 acs_capabilities_supported: None,
4479 }
4480 );
4481
4482 assert_eq!(
4484 GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
4485 GenericPcieSwitchCli {
4486 port_name: "rp0".to_string(),
4487 name: "switch0".to_string(),
4488 num_downstream_ports: 8,
4489 hotplug: true,
4490 acs_capabilities_supported: None,
4491 }
4492 );
4493
4494 assert_eq!(
4495 GenericPcieSwitchCli::from_str("rp0:switch0,acs=0").unwrap(),
4496 GenericPcieSwitchCli {
4497 port_name: "rp0".to_string(),
4498 name: "switch0".to_string(),
4499 num_downstream_ports: 4,
4500 hotplug: false,
4501 acs_capabilities_supported: Some(0),
4502 }
4503 );
4504
4505 assert_eq!(
4506 GenericPcieSwitchCli::from_str("rp0:switch0,acs=95").unwrap(),
4507 GenericPcieSwitchCli {
4508 port_name: "rp0".to_string(),
4509 name: "switch0".to_string(),
4510 num_downstream_ports: 4,
4511 hotplug: false,
4512 acs_capabilities_supported: Some(95),
4513 }
4514 );
4515
4516 assert!(GenericPcieSwitchCli::from_str("").is_err());
4518 assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
4519 assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
4520 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
4521 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
4522 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
4523 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
4524 }
4525
4526 #[test]
4527 fn test_pcie_remote_from_str() {
4528 assert_eq!(
4530 PcieRemoteCli::from_str("rc0rp0").unwrap(),
4531 PcieRemoteCli {
4532 port_name: "rc0rp0".to_string(),
4533 socket_addr: None,
4534 hu: 0,
4535 controller: 0,
4536 }
4537 );
4538
4539 assert_eq!(
4541 PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
4542 PcieRemoteCli {
4543 port_name: "rc0rp0".to_string(),
4544 socket_addr: Some("localhost:22567".to_string()),
4545 hu: 0,
4546 controller: 0,
4547 }
4548 );
4549
4550 assert_eq!(
4552 PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
4553 PcieRemoteCli {
4554 port_name: "myport".to_string(),
4555 socket_addr: Some("localhost:22568".to_string()),
4556 hu: 1,
4557 controller: 2,
4558 }
4559 );
4560
4561 assert_eq!(
4563 PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
4564 PcieRemoteCli {
4565 port_name: "port0".to_string(),
4566 socket_addr: None,
4567 hu: 5,
4568 controller: 3,
4569 }
4570 );
4571
4572 assert!(PcieRemoteCli::from_str("").is_err());
4574 assert!(PcieRemoteCli::from_str("port,socket=").is_err());
4575 assert!(PcieRemoteCli::from_str("port,hu=").is_err());
4576 assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
4577 assert!(PcieRemoteCli::from_str("port,controller=").is_err());
4578 assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
4579 assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
4580 }
4581
4582 #[test]
4583 fn test_parse_memory_units() {
4584 assert_eq!(parse_memory("64G").unwrap(), 64 * 1024 * 1024 * 1024);
4585 assert_eq!(parse_memory("64GB").unwrap(), 64 * 1024 * 1024 * 1024);
4586 assert_eq!(parse_memory("3MB").unwrap(), 3 * 1024 * 1024);
4587 assert_eq!(parse_memory("512KB").unwrap(), 512 * 1024);
4588 assert!(parse_memory("3MiB").is_err());
4589 }
4590
4591 #[test]
4592 fn test_memory_config_size_only() {
4593 assert_eq!(
4594 parse_memory_config("64G").unwrap(),
4595 MemoryCli {
4596 mem_size: 64 * 1024 * 1024 * 1024,
4597 shared: None,
4598 prefetch: false,
4599 transparent_hugepages: false,
4600 hugepages: false,
4601 hugepage_size: None,
4602 file: None,
4603 }
4604 );
4605 }
4606
4607 #[test]
4608 fn test_memory_config_key_value() {
4609 assert_eq!(
4610 parse_memory_config("size=2G,shared=off,prefetch=on,thp=on").unwrap(),
4611 MemoryCli {
4612 mem_size: 2 * 1024 * 1024 * 1024,
4613 shared: Some(false),
4614 prefetch: true,
4615 transparent_hugepages: true,
4616 hugepages: false,
4617 hugepage_size: None,
4618 file: None,
4619 }
4620 );
4621
4622 assert_eq!(
4623 parse_memory_config("size=4GB,hugepages=on,hugepage_size=2MB").unwrap(),
4624 MemoryCli {
4625 mem_size: 4 * 1024 * 1024 * 1024,
4626 shared: None,
4627 prefetch: false,
4628 transparent_hugepages: false,
4629 hugepages: true,
4630 hugepage_size: Some(2 * 1024 * 1024),
4631 file: None,
4632 }
4633 );
4634
4635 assert_eq!(
4636 parse_memory_config("file=/tmp/memory.bin").unwrap(),
4637 MemoryCli {
4638 mem_size: DEFAULT_MEMORY_SIZE,
4639 shared: None,
4640 prefetch: false,
4641 transparent_hugepages: false,
4642 hugepages: false,
4643 hugepage_size: None,
4644 file: Some(PathBuf::from("/tmp/memory.bin")),
4645 }
4646 );
4647 }
4648
4649 #[test]
4650 fn test_memory_config_rejects_invalid_combinations() {
4651 assert!(parse_memory_config("thp=on").is_err());
4652 assert!(parse_memory_config("size=1G,size=2G").is_err());
4653 assert!(parse_memory_config("hugepage_size=2M").is_err());
4654 assert!(parse_memory_config("hugepages=on,shared=off").is_err());
4655 assert!(parse_memory_config("hugepages=on,file=/tmp/memory.bin").is_err());
4656
4657 assert_eq!(
4660 parse_memory_config("hugepages=on,hugepage_size=3MB")
4661 .unwrap()
4662 .hugepage_size,
4663 Some(3 * 1024 * 1024)
4664 );
4665 }
4666
4667 #[test]
4668 fn test_memory_options_merge_legacy_aliases() {
4669 let opt = Options::try_parse_from([
4670 "openvmm",
4671 "--memory",
4672 "2G",
4673 "--prefetch",
4674 "--private-memory",
4675 "--thp",
4676 ])
4677 .unwrap();
4678 opt.validate_memory_options().unwrap();
4679 assert_eq!(opt.memory_size(), 2 * 1024 * 1024 * 1024);
4680 assert!(opt.prefetch_memory());
4681 assert!(opt.private_memory());
4682 assert!(opt.transparent_hugepages());
4683 }
4684
4685 #[test]
4686 fn test_memory_options_allow_legacy_thp_with_new_private_memory() {
4687 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=off", "--thp"]).unwrap();
4688 opt.validate_memory_options().unwrap();
4689 assert!(opt.private_memory());
4690 assert!(opt.transparent_hugepages());
4691 }
4692
4693 #[test]
4694 fn test_memory_options_reject_conflicting_legacy_aliases() {
4695 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=on", "--private-memory"])
4696 .unwrap();
4697 assert!(opt.validate_memory_options().is_err());
4698 }
4699
4700 #[test]
4701 fn test_memory_options_reject_hugepage_legacy_conflicts() {
4702 let opt =
4703 Options::try_parse_from(["openvmm", "--memory", "hugepages=on", "--private-memory"])
4704 .unwrap();
4705 assert!(opt.validate_memory_options().is_err());
4706
4707 let opt = Options::try_parse_from([
4708 "openvmm",
4709 "--memory",
4710 "hugepages=on",
4711 "--memory-backing-file",
4712 "/tmp/memory.bin",
4713 ])
4714 .unwrap();
4715 assert!(opt.validate_memory_options().is_err());
4716 }
4717
4718 #[test]
4719 fn test_pidfile_option_parsed() {
4720 let opt = Options::try_parse_from(["openvmm", "--pidfile", "/tmp/test.pid"]).unwrap();
4721 assert_eq!(opt.pidfile, Some(PathBuf::from("/tmp/test.pid")));
4722 }
4723
4724 #[cfg(target_os = "linux")]
4725 #[test]
4726 fn test_vfio_device_cli_parse() {
4727 let v = VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0").unwrap();
4729 assert_eq!(v.pci_id, "0000:01:00.0");
4730 assert_eq!(v.port_name, "rp0");
4731 assert_eq!(v.iommu, None);
4732
4733 let v = VfioDeviceCli::from_str("port=rp1,iommu=iommu0,host=0000:02:00.0").unwrap();
4735 assert_eq!(v.pci_id, "0000:02:00.0");
4736 assert_eq!(v.port_name, "rp1");
4737 assert_eq!(v.iommu.as_deref(), Some("iommu0"));
4738 }
4739
4740 #[cfg(target_os = "linux")]
4741 #[test]
4742 fn test_vfio_device_cli_errors() {
4743 assert!(VfioDeviceCli::from_str("port=rp0").is_err());
4745 assert!(VfioDeviceCli::from_str("host=0000:01:00.0").is_err());
4746
4747 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,foo=bar").is_err());
4749
4750 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,host=0000:02:00.0,port=rp0").is_err());
4752 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,port=rp1").is_err());
4753 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=a,iommu=b").is_err());
4754
4755 assert!(VfioDeviceCli::from_str("host=,port=rp0").is_err());
4757 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=").is_err());
4758 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=").is_err());
4759
4760 assert!(VfioDeviceCli::from_str("host").is_err());
4762 assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu").is_err());
4763
4764 assert!(VfioDeviceCli::from_str("host=../../etc/passwd,port=rp0").is_err());
4766 assert!(VfioDeviceCli::from_str("host=foo/bar,port=rp0").is_err());
4767 }
4768
4769 #[cfg(target_os = "linux")]
4770 #[test]
4771 fn test_iommu_cli_parse() {
4772 let c = IommuCli::from_str("id=iommu0").unwrap();
4773 assert_eq!(c.id, "iommu0");
4774
4775 assert!(IommuCli::from_str("name=iommu0").is_err());
4777
4778 assert!(IommuCli::from_str("iommu0").is_err());
4780
4781 assert!(IommuCli::from_str("id=").is_err());
4783 }
4784
4785 #[test]
4786 fn test_nvme_controller_cli_pcie() {
4787 let c = NvmeControllerCli::from_str("id=nvme0,pcie_port=p0").unwrap();
4788 assert_eq!(c.id, "nvme0");
4789 assert_eq!(c.transport, NvmeControllerTransport::Pcie("p0".into()));
4790 }
4791
4792 #[test]
4793 fn test_nvme_controller_cli_vpci_no_guid() {
4794 let c = NvmeControllerCli::from_str("id=nvme1,vpci").unwrap();
4795 assert_eq!(c.id, "nvme1");
4796 assert!(matches!(c.transport, NvmeControllerTransport::Vpci(None)));
4797 }
4798
4799 #[test]
4800 fn test_nvme_controller_cli_vpci_with_guid() {
4801 let c = NvmeControllerCli::from_str("id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c")
4802 .unwrap();
4803 assert_eq!(c.id, "nvme2");
4804 assert!(matches!(
4805 c.transport,
4806 NvmeControllerTransport::Vpci(Some(_))
4807 ));
4808 }
4809
4810 #[test]
4811 fn test_nvme_controller_cli_errors() {
4812 assert!(NvmeControllerCli::from_str("pcie_port=p0").is_err());
4814 assert!(NvmeControllerCli::from_str("id=nvme0").is_err());
4816 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,vpci").is_err());
4818 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,foo=bar").is_err());
4820 assert!(NvmeControllerCli::from_str("id=,pcie_port=p0").is_err());
4822 assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=").is_err());
4824 assert!(NvmeControllerCli::from_str("id=nvme0,vpci=not-a-guid").is_err());
4826 }
4827
4828 #[test]
4829 fn test_disk_cli_controller() {
4830 let d = DiskCli::from_str("file:disk.vhd,on=nvme0").unwrap();
4831 assert_eq!(d.controller.as_deref(), Some("nvme0"));
4832 assert_eq!(d.nsid, None);
4833 }
4834
4835 #[test]
4836 fn test_disk_cli_controller_with_nsid() {
4837 let d = DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=3").unwrap();
4838 assert_eq!(d.controller.as_deref(), Some("nvme0"));
4839 assert_eq!(d.nsid, Some(3));
4840 }
4841
4842 #[test]
4843 fn test_disk_cli_controller_errors() {
4844 assert!(DiskCli::from_str("file:disk.vhd,nsid=1").is_err());
4846 assert!(DiskCli::from_str("file:disk.vhd,lun=0").is_err());
4848 assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,pcie_port=p0").is_err());
4850 assert!(DiskCli::from_str("file:disk.vhd,on=").is_err());
4852 assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=abc").is_err());
4854 assert!(DiskCli::from_str("file:disk.vhd,on=c,nsid=1,lun=0").is_err());
4856 }
4857
4858 #[test]
4859 fn test_disk_cli_controller_with_lun() {
4860 let d = DiskCli::from_str("file:disk.vhd,on=scsi0,lun=3").unwrap();
4861 assert_eq!(d.controller.as_deref(), Some("scsi0"));
4862 assert_eq!(d.lun, Some(3));
4863 assert_eq!(d.nsid, None);
4864 }
4865
4866 #[test]
4867 fn test_scsi_controller_cli() {
4868 let c = ScsiControllerCli::from_str("id=scsi0").unwrap();
4869 assert_eq!(c.id, "scsi0");
4870 assert_eq!(c.sub_channels, 0);
4871 }
4872
4873 #[test]
4874 fn test_scsi_controller_cli_with_sub_channels() {
4875 let c = ScsiControllerCli::from_str("id=scsi1,sub_channels=4").unwrap();
4876 assert_eq!(c.id, "scsi1");
4877 assert_eq!(c.sub_channels, 4);
4878 }
4879
4880 #[test]
4881 fn test_scsi_controller_cli_errors() {
4882 assert!(ScsiControllerCli::from_str("sub_channels=4").is_err());
4884 assert!(ScsiControllerCli::from_str("id=").is_err());
4886 assert!(ScsiControllerCli::from_str("id=scsi0,foo=bar").is_err());
4888 assert!(ScsiControllerCli::from_str("id=scsi0,sub_channels=abc").is_err());
4890 }
4891
4892 #[test]
4893 fn test_disk_cli_relay() {
4894 let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt").unwrap();
4895 assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
4896 assert_eq!(d.relay.as_ref().unwrap().1, None);
4897 }
4898
4899 #[test]
4900 fn test_disk_cli_relay_with_location() {
4901 let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:3").unwrap();
4902 assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
4903 assert_eq!(d.relay.as_ref().unwrap().1, Some(3));
4904 }
4905
4906 #[test]
4907 fn test_disk_cli_relay_errors() {
4908 assert!(DiskCli::from_str("file:disk.vhd,relay=tgt").is_err());
4910 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt,uh").is_err());
4912 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:abc").is_err());
4914 assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=").is_err());
4916 }
4917
4918 #[test]
4919 fn test_nvme_controller_cli_vtl2() {
4920 let c = NvmeControllerCli::from_str("id=nvme0,vpci,vtl2").unwrap();
4921 assert_eq!(c.vtl, DeviceVtl::Vtl2);
4922 }
4923
4924 #[test]
4925 fn test_scsi_controller_cli_vtl2() {
4926 let c = ScsiControllerCli::from_str("id=scsi0,vtl2").unwrap();
4927 assert_eq!(c.vtl, DeviceVtl::Vtl2);
4928 }
4929
4930 #[test]
4931 fn test_openhcl_controller_cli() {
4932 let c = OpenhclControllerCli::from_str("id=vtl0-scsi,type=scsi").unwrap();
4933 assert_eq!(c.id, "vtl0-scsi");
4934 assert_eq!(c.controller_type, OpenhclControllerType::Scsi);
4935 assert_eq!(c.guid, None);
4936 }
4937
4938 #[test]
4939 fn test_openhcl_controller_cli_nvme_with_guid() {
4940 let c = OpenhclControllerCli::from_str(
4941 "id=vtl0-nvme,type=nvme,guid=09a59b81-2bf6-4164-81d7-3a0dc977ba65",
4942 )
4943 .unwrap();
4944 assert_eq!(c.controller_type, OpenhclControllerType::Nvme);
4945 assert!(c.guid.is_some());
4946 }
4947
4948 #[test]
4949 fn test_openhcl_controller_cli_errors() {
4950 assert!(OpenhclControllerCli::from_str("type=scsi").is_err());
4952 assert!(OpenhclControllerCli::from_str("id=foo").is_err());
4954 assert!(OpenhclControllerCli::from_str("id=foo,type=ide").is_err());
4956 assert!(OpenhclControllerCli::from_str("id=foo,type=scsi,guid=bad").is_err());
4958 }
4959
4960 #[test]
4961 fn test_parse_vp_list() {
4962 use super::parse_vp_list;
4963
4964 assert_eq!(parse_vp_list("[0,1,2,3]").unwrap(), vec![0, 1, 2, 3]);
4966
4967 assert_eq!(parse_vp_list("[5]").unwrap(), vec![5]);
4969
4970 assert_eq!(parse_vp_list("[0-3]").unwrap(), vec![0, 1, 2, 3]);
4972
4973 assert_eq!(
4975 parse_vp_list("[0,1,4-6,10]").unwrap(),
4976 vec![0, 1, 4, 5, 6, 10]
4977 );
4978
4979 assert_eq!(parse_vp_list("[0, 1, 2-4]").unwrap(), vec![0, 1, 2, 3, 4]);
4981
4982 assert!(parse_vp_list("0,1,2").is_err());
4984 assert!(parse_vp_list("0-3").is_err());
4985
4986 assert!(parse_vp_list("[3-0]").is_err());
4988
4989 assert!(parse_vp_list("[a,b]").is_err());
4991 }
4992
4993 #[test]
4994 fn test_split_options_brackets() {
4995 use super::split_options;
4996
4997 assert_eq!(
4999 split_options("a=1,b=2,c=3").unwrap(),
5000 vec!["a=1", "b=2", "c=3"]
5001 );
5002
5003 assert_eq!(
5005 split_options("size=2G,vps=[0,1,2]").unwrap(),
5006 vec!["size=2G", "vps=[0,1,2]"]
5007 );
5008
5009 assert_eq!(
5011 split_options("size=2G,vps=[0-1,4-5],host_numa_node=0").unwrap(),
5012 vec!["size=2G", "vps=[0-1,4-5]", "host_numa_node=0"]
5013 );
5014
5015 assert!(split_options("vps=[0,1").is_err());
5017 assert!(split_options("vps=0,1]").is_err());
5018 }
5019
5020 #[test]
5021 fn test_parse_numa_node() {
5022 use super::parse_numa_node;
5023
5024 let n = parse_numa_node("size=2G").unwrap();
5026 assert_eq!(n.memory.mem_size, 2 * 1024 * 1024 * 1024);
5027 assert!(n.vps.is_none());
5028 assert!(n.host_numa_node.is_none());
5029
5030 let n = parse_numa_node("size=1G,vps=[0,1,2,3]").unwrap();
5032 assert_eq!(n.vps.unwrap(), vec![0, 1, 2, 3]);
5033
5034 let n = parse_numa_node("size=1G,vps=[0-3]").unwrap();
5036 assert_eq!(n.vps.unwrap(), vec![0, 1, 2, 3]);
5037
5038 let n = parse_numa_node("size=1G,host_numa_node=1").unwrap();
5040 assert_eq!(n.host_numa_node, Some(1));
5041
5042 let n = parse_numa_node("size=1G,vps=[0,1],host_numa_node=0,hugepages=on").unwrap();
5044 assert_eq!(n.vps.unwrap(), vec![0, 1]);
5045 assert_eq!(n.host_numa_node, Some(0));
5046 assert!(n.memory.hugepages);
5047
5048 assert!(parse_numa_node("vps=[0,1]").is_err());
5050
5051 assert!(parse_numa_node("size=1G,vps=0,1").is_err());
5053
5054 assert!(parse_numa_node("size=1G,vps=[0],vps=[1]").is_err());
5056
5057 let n = parse_numa_node("size=1G,vps=[]").unwrap();
5059 assert_eq!(n.vps.unwrap(), Vec::<u32>::new());
5060 }
5061
5062 #[test]
5063 fn test_parse_numa_distance() {
5064 use super::parse_numa_distance;
5065
5066 let d = parse_numa_distance("0:1:20").unwrap();
5067 assert_eq!(d.src, 0);
5068 assert_eq!(d.dst, 1);
5069 assert_eq!(d.distance, 20);
5070
5071 let d = parse_numa_distance("0:0:10").unwrap();
5073 assert_eq!(d.distance, 10);
5074
5075 assert!(parse_numa_distance("0:1:5").is_err());
5077
5078 assert!(parse_numa_distance("0:1").is_err());
5080 assert!(parse_numa_distance("0:1:20:extra").is_err());
5081 }
5082}