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