1#![warn(missing_docs)]
20
21use anyhow::Context;
22use clap::Parser;
23use clap::ValueEnum;
24use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
25use openvmm_defs::config::DeviceVtl;
26use openvmm_defs::config::PcatBootDevice;
27use openvmm_defs::config::Vtl2BaseAddressType;
28use openvmm_defs::config::X2ApicConfig;
29use std::ffi::OsString;
30use std::net::SocketAddr;
31use std::path::PathBuf;
32use std::str::FromStr;
33use thiserror::Error;
34
35const DEFAULT_MEMORY_SIZE: u64 = 1024 * 1024 * 1024;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct MemoryCli {
40 pub mem_size: u64,
42 pub shared: Option<bool>,
44 pub prefetch: bool,
46 pub transparent_hugepages: bool,
48 pub hugepages: bool,
50 pub hugepage_size: Option<u64>,
52 pub file: Option<PathBuf>,
54}
55
56#[derive(Parser)]
61pub struct Options {
62 #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
64 pub processors: u32,
65
66 #[clap(
68 short = 'm',
69 long,
70 value_name = "PARAMS",
71 default_value = "1GB",
72 value_parser = parse_memory_config,
73 conflicts_with = "numa_memory",
74 long_help = r#"Configure guest RAM.
75
76Syntax: SIZE | key=value[,key=value...]
77
78Size suffixes accept K, M, G, and T, optionally followed by B.
79
80Options:
81 size=<SIZE> guest RAM size, default 1GB
82 shared=on|off use shared file-backed RAM, default on
83 prefetch=on|off pre-populate shared RAM mappings
84 thp=on|off mark private RAM as THP-eligible; requires shared=off
85 hugepages=on|off allocate RAM from Linux hugetlb pages
86 hugepage_size=<SIZE> hugetlb page size, default 2MB; requires hugepages=on
87 file=<PATH> use an existing file as guest RAM backing
88
89Examples:
90 --memory 4G
91 --memory size=64GB,hugepages=on,hugepage_size=2MB
92 --memory size=4G,file=path/to/memory.bin
93 --memory size=4G,shared=off,thp=on"#
94 )]
95 pub memory: MemoryCli,
96
97 #[clap(long, value_name = "SIZES", value_parser = parse_memory, value_delimiter = ',', conflicts_with = "memory")]
104 pub numa_memory: Option<Vec<u64>>,
105
106 #[clap(short = 'M', long, hide = true)]
108 pub shared_memory: bool,
109
110 #[clap(long = "prefetch", hide = true)]
112 pub deprecated_prefetch: bool,
113
114 #[clap(
118 long = "memory-backing-file",
119 value_name = "FILE",
120 hide = true,
121 conflicts_with = "deprecated_private_memory"
122 )]
123 pub deprecated_memory_backing_file: Option<PathBuf>,
124
125 #[clap(
128 long,
129 value_name = "DIR",
130 conflicts_with = "deprecated_memory_backing_file"
131 )]
132 pub restore_snapshot: Option<PathBuf>,
133
134 #[clap(long = "private-memory", hide = true, conflicts_with_all = ["deprecated_memory_backing_file", "restore_snapshot"])]
136 pub deprecated_private_memory: bool,
137
138 #[clap(long = "thp", hide = true)]
140 pub deprecated_thp: bool,
141
142 #[clap(short = 'P', long)]
144 pub paused: bool,
145
146 #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
148 pub kernel: OptionalPathBuf,
149
150 #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
152 pub initrd: OptionalPathBuf,
153
154 #[clap(short = 'c', long, value_name = "STRING")]
156 pub cmdline: Vec<String>,
157
158 #[clap(long)]
160 pub hv: bool,
161
162 #[clap(long, conflicts_with_all = ["uefi", "pcat", "igvm"])]
166 pub device_tree: bool,
167
168 #[clap(long, requires("hv"))]
172 pub vtl2: bool,
173
174 #[clap(long, requires("hv"))]
177 pub get: bool,
178
179 #[clap(long, conflicts_with("get"))]
182 pub no_get: bool,
183
184 #[clap(long, requires("vtl2"))]
186 pub no_alias_map: bool,
187
188 #[clap(long, requires("vtl2"))]
190 pub isolation: Option<IsolationCli>,
191
192 #[clap(long, value_name = "PATH", alias = "vsock-path")]
194 pub vmbus_vsock_path: Option<String>,
195
196 #[clap(long, value_name = "PATH", requires("vtl2"), alias = "vtl2-vsock-path")]
198 pub vmbus_vtl2_vsock_path: Option<String>,
199
200 #[clap(long, requires("vtl2"), default_value = "halt")]
202 pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
203
204 #[clap(long_help = r#"
206e.g: --disk memdiff:file:/path/to/disk.vhd
207
208syntax: <path> | kind:<arg>[,flag,opt=arg,...]
209
210valid disk kinds:
211 `mem:<len>` memory backed disk
212 <len>: length of ramdisk, e.g.: `1G`
213 `memdiff:<disk>` memory backed diff disk
214 <disk>: lower disk, e.g.: `file:base.img`
215 `file:<path>[;direct][;create=<len>]` file-backed disk
216 <path>: path to file
217 `;direct`: bypass the OS page cache
218 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
219 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
220 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
221 `blob:<type>:<url>` HTTP blob (read-only)
222 <type>: `flat` or `vhd1`
223 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
224 <cipher>: `xts-aes-256`
225 `prwrap:<disk>` persistent reservations wrapper
226
227flags:
228 `ro` open disk as read-only
229 `dvd` specifies that device is cd/dvd and it is read_only
230 `vtl2` assign this disk to VTL2
231 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
232 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
233
234options:
235 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
236"#)]
237 #[clap(long, value_name = "FILE")]
238 pub disk: Vec<DiskCli>,
239
240 #[clap(long_help = r#"
242e.g: --nvme memdiff:file:/path/to/disk.vhd
243
244syntax: <path> | kind:<arg>[,flag,opt=arg,...]
245
246valid disk kinds:
247 `mem:<len>` memory backed disk
248 <len>: length of ramdisk, e.g.: `1G`
249 `memdiff:<disk>` memory backed diff disk
250 <disk>: lower disk, e.g.: `file:base.img`
251 `file:<path>[;direct][;create=<len>]` file-backed disk
252 <path>: path to file
253 `;direct`: bypass the OS page cache
254 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
255 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
256 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
257 `blob:<type>:<url>` HTTP blob (read-only)
258 <type>: `flat` or `vhd1`
259 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
260 <cipher>: `xts-aes-256`
261 `prwrap:<disk>` persistent reservations wrapper
262
263flags:
264 `ro` open disk as read-only
265 `vtl2` assign this disk to VTL2
266 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
267 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
268
269options:
270 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
271"#)]
272 #[clap(long)]
273 pub nvme: Vec<DiskCli>,
274
275 #[clap(long_help = r#"
277e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
278
279syntax: <path> | kind:<arg>[,flag,opt=arg,...]
280
281valid disk kinds:
282 `mem:<len>` memory backed disk
283 <len>: length of ramdisk, e.g.: `1G`
284 `memdiff:<disk>` memory backed diff disk
285 <disk>: lower disk, e.g.: `file:base.img`
286 `file:<path>[;direct]` file-backed disk
287 <path>: path to file
288 `;direct`: bypass the OS page cache
289
290flags:
291 `ro` open disk as read-only
292
293options:
294 `pcie_port=<name>` present the disk using pcie under the specified port
295"#)]
296 #[clap(long = "virtio-blk")]
297 pub virtio_blk: Vec<DiskCli>,
298
299 #[cfg(target_os = "linux")]
324 #[clap(long = "vhost-user")]
325 pub vhost_user: Vec<VhostUserCli>,
326
327 #[clap(long, value_name = "COUNT", default_value = "0")]
329 pub scsi_sub_channels: u16,
330
331 #[clap(long)]
333 pub nic: bool,
334
335 #[clap(long)]
347 pub net: Vec<NicConfigCli>,
348
349 #[clap(long, value_name = "SWITCH_ID")]
353 pub kernel_vmnic: Vec<String>,
354
355 #[clap(long)]
357 pub gfx: bool,
358
359 #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
361 pub vtl2_gfx: bool,
362
363 #[clap(long)]
365 pub vnc: bool,
366
367 #[clap(long, value_name = "PORT", default_value = "5900")]
369 pub vnc_port: u16,
370
371 #[cfg(guest_arch = "x86_64")]
373 #[clap(long, default_value_t)]
374 pub apic_id_offset: u32,
375
376 #[clap(long)]
378 pub vps_per_socket: Option<u32>,
379
380 #[clap(long, default_value = "auto")]
382 pub smt: SmtConfigCli,
383
384 #[cfg(guest_arch = "x86_64")]
386 #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
387 pub x2apic: X2ApicConfig,
388
389 #[cfg(guest_arch = "aarch64")]
391 #[clap(long, default_value = "auto")]
392 pub gic_msi: GicMsiCli,
393
394 #[clap(long, value_name = "SERIAL")]
396 pub com1: Option<SerialConfigCli>,
397
398 #[clap(long, value_name = "SERIAL")]
400 pub com2: Option<SerialConfigCli>,
401
402 #[clap(long, value_name = "SERIAL")]
404 pub com3: Option<SerialConfigCli>,
405
406 #[clap(long, value_name = "SERIAL")]
408 pub com4: Option<SerialConfigCli>,
409
410 #[structopt(long, value_name = "SERIAL")]
412 pub vmbus_com1_serial: Option<SerialConfigCli>,
413
414 #[structopt(long, value_name = "SERIAL")]
416 pub vmbus_com2_serial: Option<SerialConfigCli>,
417
418 #[clap(long)]
420 pub serial_tx_only: bool,
421
422 #[clap(long, value_name = "SERIAL")]
424 pub debugcon: Option<DebugconSerialConfigCli>,
425
426 #[clap(long, short = 'e')]
428 pub uefi: bool,
429
430 #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
432 pub uefi_firmware: OptionalPathBuf,
433
434 #[clap(long, requires("uefi"))]
436 pub uefi_debug: bool,
437
438 #[clap(long, requires("uefi"))]
440 pub uefi_enable_memory_protections: bool,
441
442 #[clap(long, requires("pcat"))]
453 pub pcat_boot_order: Option<PcatBootOrderCli>,
454
455 #[clap(long, conflicts_with("uefi"))]
457 pub pcat: bool,
458
459 #[clap(long, requires("pcat"), value_name = "FILE")]
461 pub pcat_firmware: Option<PathBuf>,
462
463 #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
465 pub igvm: Option<PathBuf>,
466
467 #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
470 pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
471
472 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
477 pub virtio_9p: Vec<FsArgs>,
478
479 #[clap(long)]
481 pub virtio_9p_debug: bool,
482
483 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
488 pub virtio_fs: Vec<FsArgsWithOptions>,
489
490 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
495 pub virtio_fs_shmem: Vec<FsArgs>,
496
497 #[clap(long, value_name = "BUS", default_value = "auto")]
499 pub virtio_fs_bus: VirtioBusCli,
500
501 #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
506 pub virtio_pmem: Option<VirtioPmemArgs>,
507
508 #[clap(long)]
510 pub virtio_rng: bool,
511
512 #[clap(long, value_name = "BUS", default_value = "auto")]
514 pub virtio_rng_bus: VirtioBusCli,
515
516 #[clap(long, value_name = "PORT", requires("virtio_rng"))]
518 pub virtio_rng_pcie_port: Option<String>,
519
520 #[clap(long)]
526 pub virtio_console: Option<SerialConfigCli>,
527
528 #[clap(long, value_name = "PORT", requires("virtio_console"))]
530 pub virtio_console_pcie_port: Option<String>,
531
532 #[clap(long, value_name = "PATH")]
534 pub virtio_vsock_path: Option<String>,
535
536 #[clap(long)]
543 pub virtio_net: Vec<NicConfigCli>,
544
545 #[clap(long, value_name = "PATH")]
547 pub log_file: Option<PathBuf>,
548
549 #[clap(long, value_name = "PATH")]
553 pub pidfile: Option<PathBuf>,
554
555 #[clap(long, value_name = "SOCKETPATH")]
557 pub ttrpc: Option<PathBuf>,
558
559 #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
561 pub grpc: Option<PathBuf>,
562
563 #[clap(long)]
565 pub single_process: bool,
566
567 #[cfg(windows)]
569 #[clap(long, value_name = "PATH")]
570 pub device: Vec<String>,
571
572 #[clap(long, requires("uefi"))]
574 pub disable_frontpage: bool,
575
576 #[clap(long)]
578 pub tpm: bool,
579
580 #[clap(long, default_value = "control", hide(true))]
584 #[expect(clippy::option_option)]
585 pub internal_worker: Option<Option<String>>,
586
587 #[clap(long, requires("vtl2"))]
589 pub vmbus_redirect: bool,
590
591 #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
593 pub vmbus_max_version: Option<u32>,
594
595 #[clap(long_help = r#"
599e.g: --vmgs memdiff:file:/path/to/file.vmgs
600
601syntax: <path> | kind:<arg>[,flag]
602
603valid disk kinds:
604 `mem:<len>` memory backed disk
605 <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
606 `memdiff:<disk>[;create=<len>]` memory backed diff disk
607 <disk>: lower disk, e.g.: `file:base.img`
608 `file:<path>` file-backed disk
609 <path>: path to file
610
611flags:
612 `fmt` reprovision the VMGS before boot
613 `fmt-on-fail` reprovision the VMGS before boot if it is corrupted
614"#)]
615 #[clap(long)]
616 pub vmgs: Option<VmgsCli>,
617
618 #[clap(long, requires("vmgs"))]
620 pub test_gsp_by_id: bool,
621
622 #[clap(long, requires("pcat"), value_name = "FILE")]
624 pub vga_firmware: Option<PathBuf>,
625
626 #[clap(long)]
628 pub secure_boot: bool,
629
630 #[clap(long)]
632 pub secure_boot_template: Option<SecureBootTemplateCli>,
633
634 #[clap(long, value_name = "PATH")]
636 pub custom_uefi_json: Option<PathBuf>,
637
638 #[clap(long, hide(true))]
643 pub relay_console_path: Option<PathBuf>,
644
645 #[clap(long, hide(true))]
649 pub relay_console_title: Option<String>,
650
651 #[clap(long, value_name = "PORT")]
653 pub gdb: Option<u16>,
654
655 #[clap(long)]
660 pub mana: Vec<NicConfigCli>,
661
662 #[clap(long)]
676 pub hypervisor: Option<String>,
677
678 #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
686 pub custom_dsdt: Option<PathBuf>,
687
688 #[clap(long_help = r#"
698e.g: --ide memdiff:file:/path/to/disk.vhd
699
700syntax: <path> | kind:<arg>[,flag,opt=arg,...]
701
702valid disk kinds:
703 `mem:<len>` memory backed disk
704 <len>: length of ramdisk, e.g.: `1G`
705 `memdiff:<disk>` memory backed diff disk
706 <disk>: lower disk, e.g.: `file:base.img`
707 `file:<path>[;create=<len>]` file-backed disk
708 <path>: path to file
709 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
710 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
711 `blob:<type>:<url>` HTTP blob (read-only)
712 <type>: `flat` or `vhd1`
713 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
714 <cipher>: `xts-aes-256`
715
716additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
717this list is not exhaustive.
718
719flags:
720 `ro` open disk as read-only
721 `s` attach drive to secondary ide channel
722 `dvd` specifies that device is cd/dvd and it is read_only
723"#)]
724 #[clap(long, value_name = "FILE", requires("pcat"))]
725 pub ide: Vec<IdeDiskCli>,
726
727 #[clap(long_help = r#"
730e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
731
732syntax: <path> | kind:<arg>[,flag,opt=arg,...]
733
734valid disk kinds:
735 `mem:<len>` memory backed disk
736 <len>: length of ramdisk, e.g.: `1G`
737 `memdiff:<disk>` memory backed diff disk
738 <disk>: lower disk, e.g.: `file:base.img`
739 `file:<path>[;create=<len>]` file-backed disk
740 <path>: path to file
741 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
742 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
743 `blob:<type>:<url>` HTTP blob (read-only)
744 <type>: `flat` or `vhd1`
745 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
746 <cipher>: `xts-aes-256`
747
748flags:
749 `ro` open disk as read-only
750"#)]
751 #[clap(long, value_name = "FILE", requires("pcat"))]
752 pub floppy: Vec<FloppyDiskCli>,
753
754 #[clap(long)]
756 pub guest_watchdog: bool,
757
758 #[clap(long)]
760 pub openhcl_dump_path: Option<PathBuf>,
761
762 #[clap(long)]
764 pub halt_on_reset: bool,
765
766 #[clap(long)]
768 pub write_saved_state_proto: Option<PathBuf>,
769
770 #[clap(long)]
772 pub imc: Option<PathBuf>,
773
774 #[clap(long)]
776 pub battery: bool,
777
778 #[clap(long)]
780 pub uefi_console_mode: Option<UefiConsoleModeCli>,
781
782 #[clap(long_help = r#"
784Set the EFI diagnostics log level.
785
786options:
787 default default (ERROR and WARN only)
788 info info (ERROR, WARN, and INFO)
789 full full (all log levels)
790"#)]
791 #[clap(long, requires("uefi"))]
792 pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
793
794 #[clap(long)]
796 pub default_boot_always_attempt: bool,
797
798 #[clap(long_help = r#"
800Attach root complexes to the VM.
801
802Examples:
803 # Attach root complex rc0 on segment 0 with bus and MMIO ranges
804 --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
805
806Syntax: <name>[,opt=arg,...]
807
808Options:
809 `segment=<value>` configures the PCI Express segment, default 0
810 `start_bus=<value>` lowest valid bus number, default 0
811 `end_bus=<value>` highest valid bus number, default 255
812 `low_mmio=<size>` low MMIO window size, default 64M
813 `high_mmio=<size>` high MMIO window size, default 1G
814"#)]
815 #[clap(long, conflicts_with("pcat"))]
816 pub pcie_root_complex: Vec<PcieRootComplexCli>,
817
818 #[clap(long_help = r#"
820Attach root ports to root complexes.
821
822Examples:
823 # Attach root port rc0rp0 to root complex rc0
824 --pcie-root-port rc0:rc0rp0
825
826 # Attach root port rc0rp1 to root complex rc0 with hotplug support
827 --pcie-root-port rc0:rc0rp1,hotplug
828
829Syntax: <root_complex_name>:<name>[,opt,opt=arg,...]
830
831Options:
832 `hotplug` enable hotplug support for this root port
833 `acs=<mask>` ACS capability bitmask (u16, decimal or 0x-prefixed hex)
834"#)]
835 #[clap(long, conflicts_with("pcat"))]
836 pub pcie_root_port: Vec<PcieRootPortCli>,
837
838 #[clap(long_help = r#"
840Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
841
842Examples:
843 # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
844 --pcie-switch rp0:switch0,num_downstream_ports=4
845
846 # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
847 --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
848
849 # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
850 --pcie-switch rp0:switch0
851 --pcie-switch switch0-downstream-0:switch1
852 --pcie-switch switch1-downstream-1:switch2
853
854 # Enable hotplug on all downstream switch ports of switch0
855 --pcie-switch rp0:switch0,hotplug
856
857Syntax: <port_name>:<name>[,opt,opt=arg,...]
858
859 port_name can be:
860 - Root port name (e.g., "rp0") to connect directly to a root port
861 - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
862
863Options:
864 `hotplug` enable hotplug support for all downstream switch ports
865 `num_downstream_ports=<value>` number of downstream ports, default 4
866 `acs=<mask>` ACS capability bitmask for downstream switch ports
867"#)]
868 #[clap(long, conflicts_with("pcat"))]
869 pub pcie_switch: Vec<GenericPcieSwitchCli>,
870
871 #[clap(long_help = r#"
873Attach PCIe devices to root ports or downstream switch ports
874which are implemented in a simulator running in a remote process.
875
876Examples:
877 # Attach to root port rc0rp0 with default socket
878 --pcie-remote rc0rp0
879
880 # Attach with custom socket address
881 --pcie-remote rc0rp0,socket=0.0.0.0:48914
882
883 # Specify HU and controller identifiers
884 --pcie-remote rc0rp0,hu=1,controller=0
885
886 # Multiple devices on different ports
887 --pcie-remote rc0rp0,socket=0.0.0.0:48914
888 --pcie-remote rc0rp1,socket=0.0.0.0:48915
889
890Syntax: <port_name>[,opt=arg,...]
891
892Options:
893 `socket=<address>` TCP socket (default: localhost:48914)
894 `hu=<value>` Hardware unit identifier (default: 0)
895 `controller=<value>` Controller identifier (default: 0)
896"#)]
897 #[clap(long, conflicts_with("pcat"))]
898 pub pcie_remote: Vec<PcieRemoteCli>,
899
900 #[clap(long_help = r#"
902Assign a host PCI device to the guest via Linux VFIO.
903
904The device must be bound to vfio-pci on the host before starting the VM.
905
906Examples:
907 # Assign NVMe controller to root port rp0
908 --vfio rp0:0000:01:00.0
909
910Syntax: <port_name>:<pci_bdf>
911
912 port_name Root port or downstream switch port name
913 pci_bdf PCI domain:bus:device.function of the VFIO device on
914 the host (use lspci -D to find it)
915"#)]
916 #[cfg(target_os = "linux")]
917 #[clap(long, conflicts_with("pcat"))]
918 pub vfio: Vec<VfioDeviceCli>,
919}
920
921impl Options {
922 pub fn memory_size(&self) -> u64 {
924 self.memory.mem_size
925 }
926
927 pub fn prefetch_memory(&self) -> bool {
929 self.memory.prefetch || self.deprecated_prefetch
930 }
931
932 pub fn private_memory(&self) -> bool {
934 self.memory.shared == Some(false) || self.deprecated_private_memory
935 }
936
937 pub fn transparent_hugepages(&self) -> bool {
939 self.memory.transparent_hugepages || self.deprecated_thp
940 }
941
942 pub fn memory_backing_file(&self) -> Option<&PathBuf> {
944 self.memory
945 .file
946 .as_ref()
947 .or(self.deprecated_memory_backing_file.as_ref())
948 }
949
950 pub fn validate_memory_options(&self) -> anyhow::Result<()> {
952 if self.memory.file.is_some() && self.deprecated_memory_backing_file.is_some() {
953 anyhow::bail!("--memory file=... conflicts with --memory-backing-file");
954 }
955 if self.memory.file.is_some() && self.restore_snapshot.is_some() {
956 anyhow::bail!("--memory file=... conflicts with --restore-snapshot");
957 }
958 if self.memory.shared == Some(true) && self.deprecated_private_memory {
959 anyhow::bail!("--memory shared=on conflicts with --private-memory");
960 }
961 if self.memory_backing_file().is_some() && self.private_memory() {
962 anyhow::bail!("file-backed memory conflicts with private memory");
963 }
964 if self.transparent_hugepages() && !self.private_memory() {
965 anyhow::bail!("transparent huge pages requires private memory mode");
966 }
967 if self.memory.hugepages {
968 if !cfg!(target_os = "linux") {
969 anyhow::bail!("hugepages are only supported on Linux");
970 }
971 if self.private_memory() {
972 anyhow::bail!("hugepages conflict with private memory");
973 }
974 if self.memory_backing_file().is_some() || self.restore_snapshot.is_some() {
975 anyhow::bail!("hugepages conflict with file-backed memory");
976 }
977 if self.pcat {
978 anyhow::bail!("hugepages conflict with x86 legacy RAM splitting");
979 }
980 }
981 Ok(())
982 }
983}
984
985#[derive(Clone, Debug, PartialEq)]
986pub struct FsArgs {
987 pub tag: String,
988 pub path: String,
989 pub pcie_port: Option<String>,
990}
991
992impl FromStr for FsArgs {
993 type Err = anyhow::Error;
994
995 fn from_str(s: &str) -> Result<Self, Self::Err> {
996 let (pcie_port, s) = parse_pcie_port_prefix(s);
997 let mut s = s.split(',');
998 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
999 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
1000 };
1001 Ok(Self {
1002 tag: tag.to_owned(),
1003 path: path.to_owned(),
1004 pcie_port,
1005 })
1006 }
1007}
1008
1009#[derive(Clone, Debug, PartialEq)]
1010pub struct FsArgsWithOptions {
1011 pub tag: String,
1013 pub path: String,
1015 pub options: String,
1017 pub pcie_port: Option<String>,
1019}
1020
1021impl FromStr for FsArgsWithOptions {
1022 type Err = anyhow::Error;
1023
1024 fn from_str(s: &str) -> Result<Self, Self::Err> {
1025 let (pcie_port, s) = parse_pcie_port_prefix(s);
1026 let mut s = s.split(',');
1027 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
1028 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
1029 };
1030 let options = s.collect::<Vec<_>>().join(";");
1031 Ok(Self {
1032 tag: tag.to_owned(),
1033 path: path.to_owned(),
1034 options,
1035 pcie_port,
1036 })
1037 }
1038}
1039
1040#[derive(Copy, Clone, clap::ValueEnum)]
1041pub enum VirtioBusCli {
1042 Auto,
1043 Mmio,
1044 Pci,
1045 Vpci,
1046}
1047
1048fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
1053 if let Some(rest) = s.strip_prefix("pcie_port=") {
1054 if let Some((port, rest)) = rest.split_once(':') {
1055 if !port.is_empty() {
1056 return (Some(port.to_string()), rest);
1057 }
1058 }
1059 }
1060 (None, s)
1061}
1062
1063#[derive(Clone, Debug, PartialEq)]
1064pub struct VirtioPmemArgs {
1065 pub path: String,
1066 pub pcie_port: Option<String>,
1067}
1068
1069impl FromStr for VirtioPmemArgs {
1070 type Err = anyhow::Error;
1071
1072 fn from_str(s: &str) -> Result<Self, Self::Err> {
1073 let (pcie_port, s) = parse_pcie_port_prefix(s);
1074 if s.is_empty() {
1075 anyhow::bail!("expected [pcie_port=<port>:]<path>");
1076 }
1077 Ok(Self {
1078 path: s.to_owned(),
1079 pcie_port,
1080 })
1081 }
1082}
1083
1084#[derive(clap::ValueEnum, Clone, Copy)]
1085pub enum SecureBootTemplateCli {
1086 Windows,
1087 UefiCa,
1088}
1089
1090fn parse_memory(s: &str) -> anyhow::Result<u64> {
1091 if s == "VMGS_DEFAULT" {
1092 Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
1093 } else {
1094 || -> Option<u64> {
1095 let mut b = s.as_bytes();
1096 if s.ends_with('B') {
1097 b = &b[..b.len() - 1]
1098 }
1099 if b.is_empty() {
1100 return None;
1101 }
1102 let multi = match b[b.len() - 1] as char {
1103 'T' => Some(1024 * 1024 * 1024 * 1024),
1104 'G' => Some(1024 * 1024 * 1024),
1105 'M' => Some(1024 * 1024),
1106 'K' => Some(1024),
1107 _ => None,
1108 };
1109 if multi.is_some() {
1110 b = &b[..b.len() - 1]
1111 }
1112 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
1113 n.checked_mul(multi.unwrap_or(1))
1114 }()
1115 .with_context(|| format!("invalid memory size '{0}'", s))
1116 }
1117}
1118
1119fn parse_acs_capability_mask(value: &str) -> anyhow::Result<u16> {
1120 if let Some(hex) = value
1121 .strip_prefix("0x")
1122 .or_else(|| value.strip_prefix("0X"))
1123 {
1124 u16::from_str_radix(hex, 16).context("invalid ACS capability mask")
1125 } else {
1126 value.parse::<u16>().context("invalid ACS capability mask")
1127 }
1128}
1129
1130fn parse_memory_toggle(key: &str, value: &str) -> anyhow::Result<bool> {
1131 match value {
1132 "on" => Ok(true),
1133 "off" => Ok(false),
1134 _ => anyhow::bail!("invalid {key} value '{value}', expected 'on' or 'off'"),
1135 }
1136}
1137
1138fn parse_memory_config(s: &str) -> anyhow::Result<MemoryCli> {
1139 if !s.contains('=') && !s.contains(',') {
1140 return Ok(MemoryCli {
1141 mem_size: parse_memory(s)?,
1142 shared: None,
1143 prefetch: false,
1144 transparent_hugepages: false,
1145 hugepages: false,
1146 hugepage_size: None,
1147 file: None,
1148 });
1149 }
1150
1151 let mut mem_size = DEFAULT_MEMORY_SIZE;
1152 let mut saw_size = false;
1153 let mut shared = None;
1154 let mut prefetch = None;
1155 let mut transparent_hugepages = None;
1156 let mut hugepages = None;
1157 let mut hugepage_size = None;
1158 let mut file = None;
1159
1160 for part in s.split(',') {
1161 let (key, value) = part
1162 .split_once('=')
1163 .with_context(|| format!("invalid memory option '{part}', expected key=value"))?;
1164 if key.is_empty() || value.is_empty() {
1165 anyhow::bail!("invalid memory option '{part}', expected key=value");
1166 }
1167
1168 match key {
1169 "size" => {
1170 if saw_size {
1171 anyhow::bail!("duplicate memory option 'size'");
1172 }
1173 mem_size = parse_memory(value)?;
1174 saw_size = true;
1175 }
1176 "shared" => {
1177 if shared.is_some() {
1178 anyhow::bail!("duplicate memory option 'shared'");
1179 }
1180 shared = Some(parse_memory_toggle(key, value)?);
1181 }
1182 "prefetch" => {
1183 if prefetch.is_some() {
1184 anyhow::bail!("duplicate memory option 'prefetch'");
1185 }
1186 prefetch = Some(parse_memory_toggle(key, value)?);
1187 }
1188 "thp" => {
1189 if transparent_hugepages.is_some() {
1190 anyhow::bail!("duplicate memory option 'thp'");
1191 }
1192 transparent_hugepages = Some(parse_memory_toggle(key, value)?);
1193 }
1194 "hugepages" => {
1195 if hugepages.is_some() {
1196 anyhow::bail!("duplicate memory option 'hugepages'");
1197 }
1198 hugepages = Some(parse_memory_toggle(key, value)?);
1199 }
1200 "hugepage_size" => {
1201 if hugepage_size.is_some() {
1202 anyhow::bail!("duplicate memory option 'hugepage_size'");
1203 }
1204 hugepage_size = Some(parse_memory(value)?);
1205 }
1206 "file" => {
1207 if file.is_some() {
1208 anyhow::bail!("duplicate memory option 'file'");
1209 }
1210 file = Some(PathBuf::from(value));
1211 }
1212 _ => anyhow::bail!("unknown memory option '{key}'"),
1213 }
1214 }
1215
1216 if transparent_hugepages == Some(true) && shared != Some(false) {
1217 anyhow::bail!("memory thp=on requires shared=off");
1218 }
1219 if hugepage_size.is_some() && hugepages != Some(true) {
1220 anyhow::bail!("memory hugepage_size requires hugepages=on");
1221 }
1222 if hugepages == Some(true) {
1223 if shared == Some(false) {
1224 anyhow::bail!("memory hugepages=on conflicts with shared=off");
1225 }
1226 if file.is_some() {
1227 anyhow::bail!("memory hugepages=on conflicts with file=...");
1228 }
1229 }
1230
1231 Ok(MemoryCli {
1232 mem_size,
1233 shared,
1234 prefetch: prefetch.unwrap_or(false),
1235 transparent_hugepages: transparent_hugepages.unwrap_or(false),
1236 hugepages: hugepages.unwrap_or(false),
1237 hugepage_size,
1238 file,
1239 })
1240}
1241
1242fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
1244 match s.strip_prefix("0x") {
1245 Some(rest) => u64::from_str_radix(rest, 16),
1246 None => s.parse::<u64>(),
1247 }
1248}
1249
1250#[derive(Clone, Debug, PartialEq)]
1251pub enum DiskCliKind {
1252 Memory(u64),
1254 MemoryDiff(Box<DiskCliKind>),
1256 Sqlite {
1258 path: PathBuf,
1259 create_with_len: Option<u64>,
1260 },
1261 SqliteDiff {
1263 path: PathBuf,
1264 create: bool,
1265 disk: Box<DiskCliKind>,
1266 },
1267 AutoCacheSqlite {
1269 cache_path: String,
1270 key: Option<String>,
1271 disk: Box<DiskCliKind>,
1272 },
1273 PersistentReservationsWrapper(Box<DiskCliKind>),
1275 File {
1277 path: PathBuf,
1278 create_with_len: Option<u64>,
1279 direct: bool,
1280 },
1281 Blob {
1283 kind: BlobKind,
1284 url: String,
1285 },
1286 Crypt {
1288 cipher: DiskCipher,
1289 key_file: PathBuf,
1290 disk: Box<DiskCliKind>,
1291 },
1292 DelayDiskWrapper {
1294 delay_ms: u64,
1295 disk: Box<DiskCliKind>,
1296 },
1297}
1298
1299#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
1300pub enum DiskCipher {
1301 #[clap(name = "xts-aes-256")]
1302 XtsAes256,
1303}
1304
1305#[derive(Copy, Clone, Debug, PartialEq)]
1306pub enum BlobKind {
1307 Flat,
1308 Vhd1,
1309}
1310
1311struct FileOpts {
1312 path: PathBuf,
1313 create_with_len: Option<u64>,
1314 direct: bool,
1315}
1316
1317fn parse_file_opts(arg: &str) -> anyhow::Result<FileOpts> {
1318 let mut path = arg;
1319 let mut create_with_len = None;
1320 let mut direct = false;
1321
1322 if let Some((p, rest)) = arg.split_once(';') {
1324 path = p;
1325 for opt in rest.split(';') {
1326 if let Some(len) = opt.strip_prefix("create=") {
1327 create_with_len = Some(parse_memory(len)?);
1328 } else if opt == "direct" {
1329 direct = true;
1330 } else {
1331 anyhow::bail!("invalid file option '{opt}', expected 'create=<len>' or 'direct'");
1332 }
1333 }
1334 }
1335
1336 Ok(FileOpts {
1337 path: path.into(),
1338 create_with_len,
1339 direct,
1340 })
1341}
1342
1343impl DiskCliKind {
1344 fn parse_autocache(
1347 arg: &str,
1348 cache_path: Result<String, std::env::VarError>,
1349 ) -> anyhow::Result<Self> {
1350 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
1351 let cache_path = cache_path.context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
1352 Ok(DiskCliKind::AutoCacheSqlite {
1353 cache_path,
1354 key: (!key.is_empty()).then(|| key.to_string()),
1355 disk: Box::new(kind.parse()?),
1356 })
1357 }
1358}
1359
1360impl FromStr for DiskCliKind {
1361 type Err = anyhow::Error;
1362
1363 fn from_str(s: &str) -> anyhow::Result<Self> {
1364 let disk = match s.split_once(':') {
1365 None => {
1367 let FileOpts {
1368 path,
1369 create_with_len,
1370 direct,
1371 } = parse_file_opts(s)?;
1372 DiskCliKind::File {
1373 path,
1374 create_with_len,
1375 direct,
1376 }
1377 }
1378 Some((kind, arg)) => match kind {
1379 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
1380 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
1381 "sql" => {
1382 let FileOpts {
1383 path,
1384 create_with_len,
1385 direct,
1386 } = parse_file_opts(arg)?;
1387 if direct {
1388 anyhow::bail!("'direct' is not supported for 'sql' disks");
1389 }
1390 DiskCliKind::Sqlite {
1391 path,
1392 create_with_len,
1393 }
1394 }
1395 "sqldiff" => {
1396 let (path_and_opts, kind) =
1397 arg.split_once(':').context("expected path[;opts]:kind")?;
1398 let disk = Box::new(kind.parse()?);
1399 match path_and_opts.split_once(';') {
1400 Some((path, create)) => {
1401 if create != "create" {
1402 anyhow::bail!("invalid syntax after ';', expected 'create'")
1403 }
1404 DiskCliKind::SqliteDiff {
1405 path: path.into(),
1406 create: true,
1407 disk,
1408 }
1409 }
1410 None => DiskCliKind::SqliteDiff {
1411 path: path_and_opts.into(),
1412 create: false,
1413 disk,
1414 },
1415 }
1416 }
1417 "autocache" => {
1418 Self::parse_autocache(arg, std::env::var("OPENVMM_AUTO_CACHE_PATH"))?
1419 }
1420 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
1421 "file" => {
1422 let FileOpts {
1423 path,
1424 create_with_len,
1425 direct,
1426 } = parse_file_opts(arg)?;
1427 DiskCliKind::File {
1428 path,
1429 create_with_len,
1430 direct,
1431 }
1432 }
1433 "blob" => {
1434 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
1435 let blob_kind = match blob_kind {
1436 "flat" => BlobKind::Flat,
1437 "vhd1" => BlobKind::Vhd1,
1438 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
1439 };
1440 DiskCliKind::Blob {
1441 kind: blob_kind,
1442 url: url.to_string(),
1443 }
1444 }
1445 "crypt" => {
1446 let (cipher, (key, kind)) = arg
1447 .split_once(':')
1448 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
1449 .context("expected cipher:key_file:kind")?;
1450 DiskCliKind::Crypt {
1451 cipher: ValueEnum::from_str(cipher, false)
1452 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
1453 key_file: PathBuf::from(key),
1454 disk: Box::new(kind.parse()?),
1455 }
1456 }
1457 kind => {
1458 let FileOpts {
1463 path,
1464 create_with_len,
1465 direct,
1466 } = parse_file_opts(s)?;
1467 if path.has_root() {
1468 DiskCliKind::File {
1469 path,
1470 create_with_len,
1471 direct,
1472 }
1473 } else {
1474 anyhow::bail!("invalid disk kind {kind}");
1475 }
1476 }
1477 },
1478 };
1479 Ok(disk)
1480 }
1481}
1482
1483#[derive(Clone)]
1484pub struct VmgsCli {
1485 pub kind: DiskCliKind,
1486 pub provision: ProvisionVmgs,
1487}
1488
1489#[derive(Copy, Clone)]
1490pub enum ProvisionVmgs {
1491 OnEmpty,
1492 OnFailure,
1493 True,
1494}
1495
1496impl FromStr for VmgsCli {
1497 type Err = anyhow::Error;
1498
1499 fn from_str(s: &str) -> anyhow::Result<Self> {
1500 let (kind, opt) = s
1501 .split_once(',')
1502 .map(|(k, o)| (k, Some(o)))
1503 .unwrap_or((s, None));
1504 let kind = kind.parse()?;
1505
1506 let provision = match opt {
1507 None => ProvisionVmgs::OnEmpty,
1508 Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
1509 Some("fmt") => ProvisionVmgs::True,
1510 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
1511 };
1512
1513 Ok(VmgsCli { kind, provision })
1514 }
1515}
1516
1517#[derive(Clone)]
1519pub struct DiskCli {
1520 pub vtl: DeviceVtl,
1521 pub kind: DiskCliKind,
1522 pub read_only: bool,
1523 pub is_dvd: bool,
1524 pub underhill: Option<UnderhillDiskSource>,
1525 pub pcie_port: Option<String>,
1526}
1527
1528#[derive(Copy, Clone)]
1529pub enum UnderhillDiskSource {
1530 Scsi,
1531 Nvme,
1532}
1533
1534impl FromStr for DiskCli {
1535 type Err = anyhow::Error;
1536
1537 fn from_str(s: &str) -> anyhow::Result<Self> {
1538 let mut opts = s.split(',');
1539 let kind = opts.next().unwrap().parse()?;
1540
1541 let mut read_only = false;
1542 let mut is_dvd = false;
1543 let mut underhill = None;
1544 let mut vtl = DeviceVtl::Vtl0;
1545 let mut pcie_port = None;
1546 for opt in opts {
1547 let mut s = opt.split('=');
1548 let opt = s.next().unwrap();
1549 match opt {
1550 "ro" => read_only = true,
1551 "dvd" => {
1552 is_dvd = true;
1553 read_only = true;
1554 }
1555 "vtl2" => {
1556 vtl = DeviceVtl::Vtl2;
1557 }
1558 "uh" => underhill = Some(UnderhillDiskSource::Scsi),
1559 "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
1560 "pcie_port" => {
1561 let port = s.next();
1562 if port.is_none_or(|p| p.is_empty()) {
1563 anyhow::bail!("`pcie_port` requires a port name");
1564 }
1565 pcie_port = Some(String::from(port.unwrap()));
1566 }
1567 opt => anyhow::bail!("unknown option: '{opt}'"),
1568 }
1569 }
1570
1571 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
1572 anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
1573 }
1574
1575 if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
1576 anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
1577 }
1578
1579 Ok(DiskCli {
1580 vtl,
1581 kind,
1582 read_only,
1583 is_dvd,
1584 underhill,
1585 pcie_port,
1586 })
1587 }
1588}
1589
1590#[derive(Clone)]
1592pub struct IdeDiskCli {
1593 pub kind: DiskCliKind,
1594 pub read_only: bool,
1595 pub channel: Option<u8>,
1596 pub device: Option<u8>,
1597 pub is_dvd: bool,
1598}
1599
1600impl FromStr for IdeDiskCli {
1601 type Err = anyhow::Error;
1602
1603 fn from_str(s: &str) -> anyhow::Result<Self> {
1604 let mut opts = s.split(',');
1605 let kind = opts.next().unwrap().parse()?;
1606
1607 let mut read_only = false;
1608 let mut channel = None;
1609 let mut device = None;
1610 let mut is_dvd = false;
1611 for opt in opts {
1612 let mut s = opt.split('=');
1613 let opt = s.next().unwrap();
1614 match opt {
1615 "ro" => read_only = true,
1616 "p" => channel = Some(0),
1617 "s" => channel = Some(1),
1618 "0" => device = Some(0),
1619 "1" => device = Some(1),
1620 "dvd" => {
1621 is_dvd = true;
1622 read_only = true;
1623 }
1624 _ => anyhow::bail!("unknown option: '{opt}'"),
1625 }
1626 }
1627
1628 Ok(IdeDiskCli {
1629 kind,
1630 read_only,
1631 channel,
1632 device,
1633 is_dvd,
1634 })
1635 }
1636}
1637
1638#[derive(Clone, Debug, PartialEq)]
1640pub struct FloppyDiskCli {
1641 pub kind: DiskCliKind,
1642 pub read_only: bool,
1643}
1644
1645impl FromStr for FloppyDiskCli {
1646 type Err = anyhow::Error;
1647
1648 fn from_str(s: &str) -> anyhow::Result<Self> {
1649 if s.is_empty() {
1650 anyhow::bail!("empty disk spec");
1651 }
1652 let mut opts = s.split(',');
1653 let kind = opts.next().unwrap().parse()?;
1654
1655 let mut read_only = false;
1656 for opt in opts {
1657 let mut s = opt.split('=');
1658 let opt = s.next().unwrap();
1659 match opt {
1660 "ro" => read_only = true,
1661 _ => anyhow::bail!("unknown option: '{opt}'"),
1662 }
1663 }
1664
1665 Ok(FloppyDiskCli { kind, read_only })
1666 }
1667}
1668
1669#[derive(Clone)]
1670pub struct DebugconSerialConfigCli {
1671 pub port: u16,
1672 pub serial: SerialConfigCli,
1673}
1674
1675impl FromStr for DebugconSerialConfigCli {
1676 type Err = String;
1677
1678 fn from_str(s: &str) -> Result<Self, Self::Err> {
1679 let Some((port, serial)) = s.split_once(',') else {
1680 return Err("invalid format (missing comma between port and serial)".into());
1681 };
1682
1683 let port: u16 = parse_number(port)
1684 .map_err(|_| "could not parse port".to_owned())?
1685 .try_into()
1686 .map_err(|_| "port must be 16-bit")?;
1687 let serial: SerialConfigCli = serial.parse()?;
1688
1689 Ok(Self { port, serial })
1690 }
1691}
1692
1693#[derive(Clone, Debug, PartialEq)]
1695pub enum SerialConfigCli {
1696 None,
1697 Console,
1698 NewConsole(Option<PathBuf>, Option<String>),
1699 Stderr,
1700 Pipe(PathBuf),
1701 Tcp(SocketAddr),
1702 File(PathBuf),
1703}
1704
1705impl FromStr for SerialConfigCli {
1706 type Err = String;
1707
1708 fn from_str(s: &str) -> Result<Self, Self::Err> {
1709 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1710
1711 let first_key = match keyvalues.first() {
1712 Some(first_pair) => first_pair.0.as_str(),
1713 None => Err("invalid serial configuration: no values supplied")?,
1714 };
1715 let first_value = keyvalues.first().unwrap().1.as_ref();
1716
1717 let ret = match first_key {
1718 "none" => SerialConfigCli::None,
1719 "console" => SerialConfigCli::Console,
1720 "stderr" => SerialConfigCli::Stderr,
1721 "file" => match first_value {
1722 Some(path) => SerialConfigCli::File(path.into()),
1723 None => Err("invalid serial configuration: file requires a value")?,
1724 },
1725 "term" => {
1726 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1728 let window_name = match window_name {
1729 Some((_, Some(name))) => Some(name.clone()),
1730 _ => None,
1731 };
1732
1733 SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
1734 }
1735 "listen" => match first_value {
1736 Some(path) => {
1737 if let Some(tcp) = path.strip_prefix("tcp:") {
1738 let addr = tcp
1739 .parse()
1740 .map_err(|err| format!("invalid tcp address: {err}"))?;
1741 SerialConfigCli::Tcp(addr)
1742 } else {
1743 SerialConfigCli::Pipe(path.into())
1744 }
1745 }
1746 None => Err(
1747 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1748 )?,
1749 },
1750 _ => {
1751 return Err(format!(
1752 "invalid serial configuration: '{}' is not a known option",
1753 first_key
1754 ));
1755 }
1756 };
1757
1758 Ok(ret)
1759 }
1760}
1761
1762impl SerialConfigCli {
1763 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1766 let mut ret = Vec::new();
1767
1768 for item in s.split(',') {
1770 let mut eqsplit = item.split('=');
1773 let key = eqsplit.next();
1774 let value = eqsplit.next();
1775
1776 if let Some(key) = key {
1777 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1778 } else {
1779 return Err("invalid key=value pair in serial config".into());
1781 }
1782 }
1783 Ok(ret)
1784 }
1785}
1786
1787#[derive(Clone, Debug, PartialEq)]
1788pub enum EndpointConfigCli {
1789 None,
1790 Consomme {
1791 cidr: Option<String>,
1792 host_fwd: Vec<HostPortConfigCli>,
1793 },
1794 Dio {
1795 id: Option<String>,
1796 },
1797 Tap {
1798 name: String,
1799 },
1800}
1801
1802#[derive(Clone, Debug, PartialEq)]
1804pub struct HostPortConfigCli {
1805 pub protocol: HostPortProtocolCli,
1806 pub host_address: Option<std::net::IpAddr>,
1807 pub host_port: u16,
1808 pub guest_port: u16,
1809}
1810
1811#[derive(Clone, Debug, PartialEq)]
1813pub enum HostPortProtocolCli {
1814 Tcp,
1815 Udp,
1816}
1817
1818fn parse_hostfwd(s: &str) -> Result<HostPortConfigCli, String> {
1819 let (host_part, guest_part) = s.split_once('-').ok_or_else(|| {
1822 format!(
1823 "invalid hostfwd format '{s}', \
1824 expected 'proto:[hostaddr]:hostport-[guestaddr]:guestport'"
1825 )
1826 })?;
1827
1828 let (proto, host_addr_port) = host_part.split_once(':').ok_or_else(|| {
1830 format!("invalid hostfwd host part '{host_part}', expected 'proto:[hostaddr]:hostport'")
1831 })?;
1832 let protocol = match proto {
1833 "tcp" => HostPortProtocolCli::Tcp,
1834 "udp" => HostPortProtocolCli::Udp,
1835 other => {
1836 return Err(format!(
1837 "unknown hostfwd protocol '{other}', expected 'tcp' or 'udp'"
1838 ));
1839 }
1840 };
1841
1842 let (host_address, host_port) = parse_addr_port(host_addr_port)
1843 .map_err(|e| format!("invalid hostfwd host address/port: {e}"))?;
1844 let (_, guest_port) = parse_addr_port(guest_part)
1845 .map_err(|e| format!("invalid hostfwd guest address/port: {e}"))?;
1846
1847 Ok(HostPortConfigCli {
1848 protocol,
1849 host_address,
1850 host_port,
1851 guest_port,
1852 })
1853}
1854
1855fn parse_addr_port(s: &str) -> Result<(Option<std::net::IpAddr>, u16), String> {
1861 if let Some(rest) = s.strip_prefix('[') {
1862 let (addr, port) = rest
1864 .split_once("]:")
1865 .ok_or_else(|| format!("expected '[addr]:port', got '[{rest}'"))?;
1866 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
1867 let addr: std::net::IpAddr = addr
1868 .parse()
1869 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
1870 Ok((Some(addr), port))
1871 } else {
1872 match s.rsplit_once(':') {
1873 Some((addr, port)) => {
1874 let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
1875 let addr = if addr.is_empty() {
1876 None
1877 } else {
1878 let parsed: std::net::IpAddr = addr
1879 .parse()
1880 .map_err(|e| format!("invalid address '{addr}': {e}"))?;
1881 Some(parsed)
1882 };
1883 Ok((addr, port))
1884 }
1885 None => {
1886 let port: u16 = s.parse().map_err(|_| format!("invalid port '{s}'"))?;
1887 Ok((None, port))
1888 }
1889 }
1890 }
1891}
1892
1893impl FromStr for EndpointConfigCli {
1894 type Err = String;
1895
1896 fn from_str(s: &str) -> Result<Self, Self::Err> {
1897 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1898 ["none"] => EndpointConfigCli::None,
1899 ["consomme", rest @ ..] => {
1900 let remaining = rest.join(":");
1901 let mut cidr = None;
1902 let mut host_fwd = Vec::new();
1903 for opt in remaining.split(',').filter(|s| !s.is_empty()) {
1904 if let Some(fwd) = opt.strip_prefix("hostfwd=") {
1905 host_fwd.push(parse_hostfwd(fwd)?);
1906 } else if cidr.is_none() {
1907 cidr = Some(opt.to_owned());
1908 } else {
1909 return Err(format!("unexpected consomme option '{opt}'"));
1910 }
1911 }
1912 EndpointConfigCli::Consomme { cidr, host_fwd }
1913 }
1914 ["dio", s @ ..] => EndpointConfigCli::Dio {
1915 id: s.first().map(|s| (*s).to_owned()),
1916 },
1917 ["tap", name] => EndpointConfigCli::Tap {
1918 name: (*name).to_owned(),
1919 },
1920 _ => return Err("invalid network backend".into()),
1921 };
1922
1923 Ok(ret)
1924 }
1925}
1926
1927#[derive(Clone, Debug, PartialEq)]
1928pub struct NicConfigCli {
1929 pub vtl: DeviceVtl,
1930 pub endpoint: EndpointConfigCli,
1931 pub max_queues: Option<u16>,
1932 pub underhill: bool,
1933 pub pcie_port: Option<String>,
1934}
1935
1936impl FromStr for NicConfigCli {
1937 type Err = String;
1938
1939 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1940 let mut vtl = DeviceVtl::Vtl0;
1941 let mut max_queues = None;
1942 let mut underhill = false;
1943 let mut pcie_port = None;
1944 while let Some((opt, rest)) = s.split_once(':') {
1945 if let Some((opt, val)) = opt.split_once('=') {
1946 match opt {
1947 "queues" => {
1948 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1949 }
1950 "pcie_port" => {
1951 if val.is_empty() {
1952 return Err("`pcie_port=` requires port name argument".into());
1953 }
1954 pcie_port = Some(val.to_string());
1955 }
1956 _ => break,
1957 }
1958 } else {
1959 match opt {
1960 "vtl2" => {
1961 vtl = DeviceVtl::Vtl2;
1962 }
1963 "uh" => underhill = true,
1964 _ => break,
1965 }
1966 }
1967 s = rest;
1968 }
1969
1970 if underhill && vtl != DeviceVtl::Vtl0 {
1971 return Err("`uh` is incompatible with `vtl2`".into());
1972 }
1973
1974 if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
1975 return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
1976 }
1977
1978 let endpoint = s.parse()?;
1979 Ok(NicConfigCli {
1980 vtl,
1981 endpoint,
1982 max_queues,
1983 underhill,
1984 pcie_port,
1985 })
1986 }
1987}
1988
1989#[derive(Debug, Error)]
1990#[error("unknown VTL2 relocation type: {0}")]
1991pub struct UnknownVtl2RelocationType(String);
1992
1993fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1994 match s {
1995 "disable" => Ok(Vtl2BaseAddressType::File),
1996 s if s.starts_with("auto=") => {
1997 let s = s.strip_prefix("auto=").unwrap_or_default();
1998 let size = if s == "filesize" {
1999 None
2000 } else {
2001 let size = parse_memory(s).map_err(|e| {
2002 UnknownVtl2RelocationType(format!(
2003 "unable to parse memory size from {} for 'auto=' type, {e}",
2004 e
2005 ))
2006 })?;
2007 Some(size)
2008 };
2009 Ok(Vtl2BaseAddressType::MemoryLayout { size })
2010 }
2011 s if s.starts_with("absolute=") => {
2012 let s = s.strip_prefix("absolute=");
2013 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
2014 UnknownVtl2RelocationType(format!(
2015 "unable to parse number from {} for 'absolute=' type",
2016 e
2017 ))
2018 })?;
2019 Ok(Vtl2BaseAddressType::Absolute(addr))
2020 }
2021 s if s.starts_with("vtl2=") => {
2022 let s = s.strip_prefix("vtl2=").unwrap_or_default();
2023 let size = if s == "filesize" {
2024 None
2025 } else {
2026 let size = parse_memory(s).map_err(|e| {
2027 UnknownVtl2RelocationType(format!(
2028 "unable to parse memory size from {} for 'vtl2=' type, {e}",
2029 e
2030 ))
2031 })?;
2032 Some(size)
2033 };
2034 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
2035 }
2036 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
2037 }
2038}
2039
2040#[derive(Debug, Copy, Clone, PartialEq)]
2041pub enum SmtConfigCli {
2042 Auto,
2043 Force,
2044 Off,
2045}
2046
2047#[derive(Debug, Error)]
2048#[error("expected auto, force, or off")]
2049pub struct BadSmtConfig;
2050
2051impl FromStr for SmtConfigCli {
2052 type Err = BadSmtConfig;
2053
2054 fn from_str(s: &str) -> Result<Self, Self::Err> {
2055 let r = match s {
2056 "auto" => Self::Auto,
2057 "force" => Self::Force,
2058 "off" => Self::Off,
2059 _ => return Err(BadSmtConfig),
2060 };
2061 Ok(r)
2062 }
2063}
2064
2065#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
2066fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
2067 let r = match s {
2068 "auto" => X2ApicConfig::Auto,
2069 "supported" => X2ApicConfig::Supported,
2070 "off" => X2ApicConfig::Unsupported,
2071 "on" => X2ApicConfig::Enabled,
2072 _ => return Err("expected auto, supported, off, or on"),
2073 };
2074 Ok(r)
2075}
2076
2077#[derive(Debug, Copy, Clone, ValueEnum)]
2078pub enum Vtl0LateMapPolicyCli {
2079 Off,
2080 Log,
2081 Halt,
2082 Exception,
2083}
2084
2085#[derive(Debug, Copy, Clone, Default, ValueEnum)]
2087pub enum GicMsiCli {
2088 #[default]
2090 Auto,
2091 Its,
2093 V2m,
2095}
2096
2097#[derive(Debug, Copy, Clone, ValueEnum)]
2098pub enum IsolationCli {
2099 Vbs,
2100}
2101
2102#[derive(Debug, Copy, Clone, PartialEq)]
2103pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
2104
2105impl FromStr for PcatBootOrderCli {
2106 type Err = &'static str;
2107
2108 fn from_str(s: &str) -> Result<Self, Self::Err> {
2109 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
2110 let mut order = Vec::new();
2111
2112 for item in s.split(',') {
2113 let device = match item {
2114 "optical" => PcatBootDevice::Optical,
2115 "hdd" => PcatBootDevice::HardDrive,
2116 "net" => PcatBootDevice::Network,
2117 "floppy" => PcatBootDevice::Floppy,
2118 _ => return Err("unknown boot device type"),
2119 };
2120
2121 let default_pos = default_order
2122 .iter()
2123 .position(|x| x == &Some(device))
2124 .ok_or("cannot pass duplicate boot devices")?;
2125
2126 order.push(default_order[default_pos].take().unwrap());
2127 }
2128
2129 order.extend(default_order.into_iter().flatten());
2130 assert_eq!(order.len(), 4);
2131
2132 Ok(Self(order.try_into().unwrap()))
2133 }
2134}
2135
2136#[derive(Copy, Clone, Debug, ValueEnum)]
2137pub enum UefiConsoleModeCli {
2138 Default,
2139 Com1,
2140 Com2,
2141 None,
2142}
2143
2144#[derive(Copy, Clone, Debug, Default, ValueEnum)]
2145pub enum EfiDiagnosticsLogLevelCli {
2146 #[default]
2147 Default,
2148 Info,
2149 Full,
2150}
2151
2152#[derive(Clone, Debug, PartialEq)]
2153pub struct PcieRootComplexCli {
2154 pub name: String,
2155 pub segment: u16,
2156 pub start_bus: u8,
2157 pub end_bus: u8,
2158 pub low_mmio: u32,
2159 pub high_mmio: u64,
2160}
2161
2162impl FromStr for PcieRootComplexCli {
2163 type Err = anyhow::Error;
2164
2165 fn from_str(s: &str) -> Result<Self, Self::Err> {
2166 const DEFAULT_PCIE_CRS_LOW_SIZE: u32 = 64 * 1024 * 1024; const DEFAULT_PCIE_CRS_HIGH_SIZE: u64 = 1024 * 1024 * 1024; let mut opts = s.split(',');
2170 let name = opts.next().context("expected root complex name")?;
2171 if name.is_empty() {
2172 anyhow::bail!("must provide a root complex name");
2173 }
2174
2175 let mut segment = 0;
2176 let mut start_bus = 0;
2177 let mut end_bus = 255;
2178 let mut low_mmio = DEFAULT_PCIE_CRS_LOW_SIZE;
2179 let mut high_mmio = DEFAULT_PCIE_CRS_HIGH_SIZE;
2180 for opt in opts {
2181 let mut s = opt.split('=');
2182 let opt = s.next().context("expected option")?;
2183 match opt {
2184 "segment" => {
2185 let seg_str = s.next().context("expected segment number")?;
2186 segment = u16::from_str(seg_str).context("failed to parse segment number")?;
2187 }
2188 "start_bus" => {
2189 let bus_str = s.next().context("expected start bus number")?;
2190 start_bus =
2191 u8::from_str(bus_str).context("failed to parse start bus number")?;
2192 }
2193 "end_bus" => {
2194 let bus_str = s.next().context("expected end bus number")?;
2195 end_bus = u8::from_str(bus_str).context("failed to parse end bus number")?;
2196 }
2197 "low_mmio" => {
2198 let low_mmio_str = s.next().context("expected low MMIO size")?;
2199 low_mmio = parse_memory(low_mmio_str)
2200 .context("failed to parse low MMIO size")?
2201 .try_into()?;
2202 }
2203 "high_mmio" => {
2204 let high_mmio_str = s.next().context("expected high MMIO size")?;
2205 high_mmio =
2206 parse_memory(high_mmio_str).context("failed to parse high MMIO size")?;
2207 }
2208 opt => anyhow::bail!("unknown option: '{opt}'"),
2209 }
2210 }
2211
2212 if start_bus >= end_bus {
2213 anyhow::bail!("start_bus must be less than or equal to end_bus");
2214 }
2215
2216 Ok(PcieRootComplexCli {
2217 name: name.to_string(),
2218 segment,
2219 start_bus,
2220 end_bus,
2221 low_mmio,
2222 high_mmio,
2223 })
2224 }
2225}
2226
2227#[derive(Clone, Debug, PartialEq)]
2228pub struct PcieRootPortCli {
2229 pub root_complex_name: String,
2230 pub name: String,
2231 pub hotplug: bool,
2232 pub acs_capabilities_supported: Option<u16>,
2233}
2234
2235impl FromStr for PcieRootPortCli {
2236 type Err = anyhow::Error;
2237
2238 fn from_str(s: &str) -> Result<Self, Self::Err> {
2239 let mut opts = s.split(',');
2240 let names = opts.next().context("expected root port identifiers")?;
2241 if names.is_empty() {
2242 anyhow::bail!("must provide root port identifiers");
2243 }
2244
2245 let mut s = names.split(':');
2246 let rc_name = s.next().context("expected name of parent root complex")?;
2247 let rp_name = s.next().context("expected root port name")?;
2248
2249 if let Some(extra) = s.next() {
2250 anyhow::bail!("unexpected token: '{extra}'")
2251 }
2252
2253 let mut hotplug = false;
2254 let mut acs_capabilities_supported = None;
2255
2256 for opt in opts {
2258 let mut kv = opt.split('=');
2259 let key = kv.next().context("expected option name")?;
2260 let value = kv.next();
2261
2262 match key {
2263 "hotplug" => {
2264 if value.is_some() {
2265 anyhow::bail!("hotplug option does not take a value")
2266 }
2267 hotplug = true;
2268 }
2269 "acs" => {
2270 let value = value.context("acs option requires a value")?;
2271 if kv.next().is_some() {
2272 anyhow::bail!("acs option expects a single value")
2273 }
2274 acs_capabilities_supported = Some(parse_acs_capability_mask(value)?);
2275 }
2276 _ => anyhow::bail!("unexpected option: '{opt}'"),
2277 }
2278 }
2279
2280 Ok(PcieRootPortCli {
2281 root_complex_name: rc_name.to_string(),
2282 name: rp_name.to_string(),
2283 hotplug,
2284 acs_capabilities_supported,
2285 })
2286 }
2287}
2288
2289#[derive(Clone, Debug, PartialEq)]
2290pub struct GenericPcieSwitchCli {
2291 pub port_name: String,
2292 pub name: String,
2293 pub num_downstream_ports: u8,
2294 pub hotplug: bool,
2295 pub acs_capabilities_supported: Option<u16>,
2296}
2297
2298impl FromStr for GenericPcieSwitchCli {
2299 type Err = anyhow::Error;
2300
2301 fn from_str(s: &str) -> Result<Self, Self::Err> {
2302 let mut opts = s.split(',');
2303 let names = opts.next().context("expected switch identifiers")?;
2304 if names.is_empty() {
2305 anyhow::bail!("must provide switch identifiers");
2306 }
2307
2308 let mut s = names.split(':');
2309 let port_name = s.next().context("expected name of parent port")?;
2310 let switch_name = s.next().context("expected switch name")?;
2311
2312 if let Some(extra) = s.next() {
2313 anyhow::bail!("unexpected token: '{extra}'")
2314 }
2315
2316 let mut num_downstream_ports = 4u8; let mut hotplug = false;
2318 let mut acs_capabilities_supported = None;
2319
2320 for opt in opts {
2321 let mut kv = opt.split('=');
2322 let key = kv.next().context("expected option name")?;
2323
2324 match key {
2325 "num_downstream_ports" => {
2326 let value = kv.next().context("expected option value")?;
2327 if let Some(extra) = kv.next() {
2328 anyhow::bail!("unexpected token: '{extra}'")
2329 }
2330 num_downstream_ports = value.parse().context("invalid num_downstream_ports")?;
2331 }
2332 "hotplug" => {
2333 if kv.next().is_some() {
2334 anyhow::bail!("hotplug option does not take a value")
2335 }
2336 hotplug = true;
2337 }
2338 "acs" => {
2339 let value = kv.next().context("acs option requires a value")?;
2340 if kv.next().is_some() {
2341 anyhow::bail!("acs option expects a single value")
2342 }
2343 acs_capabilities_supported = Some(parse_acs_capability_mask(value)?);
2344 }
2345 _ => anyhow::bail!("unknown option: '{key}'"),
2346 }
2347 }
2348
2349 Ok(GenericPcieSwitchCli {
2350 port_name: port_name.to_string(),
2351 name: switch_name.to_string(),
2352 num_downstream_ports,
2353 hotplug,
2354 acs_capabilities_supported,
2355 })
2356 }
2357}
2358
2359#[derive(Clone, Debug, PartialEq)]
2361pub struct PcieRemoteCli {
2362 pub port_name: String,
2364 pub socket_addr: Option<String>,
2366 pub hu: u16,
2368 pub controller: u16,
2370}
2371
2372impl FromStr for PcieRemoteCli {
2373 type Err = anyhow::Error;
2374
2375 fn from_str(s: &str) -> Result<Self, Self::Err> {
2376 let mut opts = s.split(',');
2377 let port_name = opts.next().context("expected port name")?;
2378 if port_name.is_empty() {
2379 anyhow::bail!("must provide a port name");
2380 }
2381
2382 let mut socket_addr = None;
2383 let mut hu = 0u16;
2384 let mut controller = 0u16;
2385
2386 for opt in opts {
2387 let mut kv = opt.split('=');
2388 let key = kv.next().context("expected option name")?;
2389 let value = kv.next();
2390
2391 match key {
2392 "socket" => {
2393 let addr = value.context("socket requires an address")?;
2394 if let Some(extra) = kv.next() {
2395 anyhow::bail!("unexpected token: '{extra}'")
2396 }
2397 if addr.is_empty() {
2398 anyhow::bail!("socket address cannot be empty");
2399 }
2400 socket_addr = Some(addr.to_string());
2401 }
2402 "hu" => {
2403 let val = value.context("hu requires a value")?;
2404 if let Some(extra) = kv.next() {
2405 anyhow::bail!("unexpected token: '{extra}'")
2406 }
2407 hu = val.parse().context("failed to parse hu")?;
2408 }
2409 "controller" => {
2410 let val = value.context("controller requires a value")?;
2411 if let Some(extra) = kv.next() {
2412 anyhow::bail!("unexpected token: '{extra}'")
2413 }
2414 controller = val.parse().context("failed to parse controller")?;
2415 }
2416 _ => anyhow::bail!("unknown option: '{key}'"),
2417 }
2418 }
2419
2420 Ok(PcieRemoteCli {
2421 port_name: port_name.to_string(),
2422 socket_addr,
2423 hu,
2424 controller,
2425 })
2426 }
2427}
2428
2429#[cfg(target_os = "linux")]
2431#[derive(Clone, Debug)]
2432pub struct VfioDeviceCli {
2433 pub port_name: String,
2435 pub pci_id: String,
2437}
2438
2439#[cfg(target_os = "linux")]
2440impl FromStr for VfioDeviceCli {
2441 type Err = anyhow::Error;
2442
2443 fn from_str(s: &str) -> Result<Self, Self::Err> {
2444 let (port_name, pci_id) = s
2445 .split_once(':')
2446 .context("expected <port_name>:<pci_bdf> (e.g., rp0:0000:01:00.0)")?;
2447
2448 if port_name.is_empty() {
2449 anyhow::bail!("port name cannot be empty");
2450 }
2451
2452 if pci_id.is_empty() {
2453 anyhow::bail!("PCI address cannot be empty");
2454 }
2455
2456 if pci_id.contains('/') || pci_id.contains("..") {
2458 anyhow::bail!("PCI address must not contain path separators");
2459 }
2460
2461 Ok(VfioDeviceCli {
2462 port_name: port_name.to_string(),
2463 pci_id: pci_id.to_string(),
2464 })
2465 }
2466}
2467
2468fn default_value_from_arch_env(name: &str) -> OsString {
2476 let prefix = if cfg!(guest_arch = "x86_64") {
2477 "X86_64"
2478 } else if cfg!(guest_arch = "aarch64") {
2479 "AARCH64"
2480 } else {
2481 return Default::default();
2482 };
2483 let prefixed = format!("{}_{}", prefix, name);
2484 std::env::var_os(name)
2485 .or_else(|| std::env::var_os(prefixed))
2486 .unwrap_or_default()
2487}
2488
2489#[derive(Clone)]
2491pub struct OptionalPathBuf(pub Option<PathBuf>);
2492
2493impl From<&std::ffi::OsStr> for OptionalPathBuf {
2494 fn from(s: &std::ffi::OsStr) -> Self {
2495 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
2496 }
2497}
2498
2499#[cfg(target_os = "linux")]
2500#[derive(Clone)]
2501pub enum VhostUserDeviceTypeCli {
2502 Blk {
2505 num_queues: Option<u16>,
2506 queue_size: Option<u16>,
2507 },
2508 Fs {
2510 tag: String,
2511 num_queues: Option<u16>,
2512 queue_size: Option<u16>,
2513 },
2514 Other {
2516 device_id: u16,
2517 queue_sizes: Vec<u16>,
2518 },
2519}
2520
2521#[cfg(target_os = "linux")]
2522#[derive(Clone)]
2523pub struct VhostUserCli {
2524 pub socket_path: String,
2525 pub device_type: VhostUserDeviceTypeCli,
2526 pub pcie_port: Option<String>,
2527}
2528
2529#[cfg(target_os = "linux")]
2533fn split_respecting_brackets(s: &str) -> anyhow::Result<Vec<&str>> {
2534 let mut result = Vec::new();
2535 let mut start = 0;
2536 let mut depth: i32 = 0;
2537 for (i, c) in s.char_indices() {
2538 match c {
2539 '[' => depth += 1,
2540 ']' => {
2541 depth -= 1;
2542 anyhow::ensure!(depth >= 0, "unmatched ']' in option string");
2543 }
2544 ',' if depth == 0 => {
2545 result.push(&s[start..i]);
2546 start = i + 1;
2547 }
2548 _ => {}
2549 }
2550 }
2551 anyhow::ensure!(depth == 0, "unclosed '[' in option string");
2552 result.push(&s[start..]);
2553 Ok(result)
2554}
2555
2556#[cfg(target_os = "linux")]
2557impl FromStr for VhostUserCli {
2558 type Err = anyhow::Error;
2559
2560 fn from_str(s: &str) -> anyhow::Result<Self> {
2561 let parts = split_respecting_brackets(s)?;
2563 let mut parts_iter = parts.into_iter();
2564 let socket_path = parts_iter
2565 .next()
2566 .context("missing socket path")?
2567 .to_string();
2568
2569 let mut device_id: Option<u16> = None;
2570 let mut tag: Option<String> = None;
2571 let mut pcie_port: Option<String> = None;
2572 let mut type_name = None;
2573 let mut num_queues: Option<u16> = None;
2574 let mut queue_size: Option<u16> = None;
2575 let mut queue_sizes: Option<Vec<u16>> = None;
2576 for opt in parts_iter {
2577 let (key, val) = opt.split_once('=').context("expected key=value option")?;
2578 match key {
2579 "type" => {
2580 type_name = Some(val);
2581 }
2582 "device_id" => {
2583 device_id = Some(val.parse().context("invalid device_id")?);
2584 }
2585 "tag" => {
2586 tag = Some(val.to_string());
2587 }
2588 "pcie_port" => {
2589 pcie_port = Some(val.to_string());
2590 }
2591 "num_queues" => {
2592 num_queues = Some(val.parse().context("invalid num_queues")?);
2593 }
2594 "queue_size" => {
2595 queue_size = Some(val.parse().context("invalid queue_size")?);
2596 }
2597 "queue_sizes" => {
2598 let trimmed = val
2600 .strip_prefix('[')
2601 .and_then(|v| v.strip_suffix(']'))
2602 .context("queue_sizes must be bracketed: [N,N,N]")?;
2603 let sizes: Vec<u16> = trimmed
2604 .split(',')
2605 .map(|s| s.parse().context("invalid queue size in queue_sizes"))
2606 .collect::<anyhow::Result<_>>()?;
2607 anyhow::ensure!(!sizes.is_empty(), "queue_sizes must be non-empty");
2608 queue_sizes = Some(sizes);
2609 }
2610 other => anyhow::bail!("unknown vhost-user option: '{other}'"),
2611 }
2612 }
2613
2614 if type_name.is_some() == device_id.is_some() {
2615 anyhow::bail!("must specify type=<name> or device_id=<N>");
2616 }
2617
2618 let device_type = match type_name {
2620 Some("fs") => {
2621 let tag = tag.take().context("type=fs requires tag=<name>")?;
2622 VhostUserDeviceTypeCli::Fs {
2623 tag,
2624 num_queues: num_queues.take(),
2625 queue_size: queue_size.take(),
2626 }
2627 }
2628 Some("blk") => VhostUserDeviceTypeCli::Blk {
2629 num_queues: num_queues.take(),
2630 queue_size: queue_size.take(),
2631 },
2632 Some(ty) => anyhow::bail!("unknown vhost-user device type: '{ty}'"),
2633 None => {
2634 let queue_sizes = queue_sizes
2635 .take()
2636 .context("device_id= requires queue_sizes=[N,N,...]")?;
2637 VhostUserDeviceTypeCli::Other {
2638 device_id: device_id.unwrap(),
2639 queue_sizes,
2640 }
2641 }
2642 };
2643
2644 if tag.is_some() {
2645 anyhow::bail!("tag= is only valid for type=fs");
2646 }
2647 if queue_sizes.is_some() {
2648 anyhow::bail!("queue_sizes= is only valid for device_id=");
2649 }
2650 if num_queues.is_some() || queue_size.is_some() {
2651 anyhow::bail!(
2652 "num_queues= and queue_size= are not valid for device_id=; use queue_sizes="
2653 );
2654 }
2655
2656 Ok(VhostUserCli {
2657 socket_path,
2658 device_type,
2659 pcie_port,
2660 })
2661 }
2662}
2663
2664#[cfg(test)]
2665mod tests {
2666 use super::*;
2667
2668 use std::path::Path;
2669
2670 #[test]
2671 fn test_parse_file_opts() {
2672 let disk = DiskCliKind::from_str("file:test.vhd;create=1G").unwrap();
2674 assert!(matches!(
2675 &disk,
2676 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
2677 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
2678 ));
2679
2680 let disk = DiskCliKind::from_str("test.vhd;create=1G").unwrap();
2682 assert!(matches!(
2683 &disk,
2684 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
2685 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
2686 ));
2687
2688 let disk = DiskCliKind::from_str("file:/dev/sdb;direct").unwrap();
2690 assert!(matches!(
2691 &disk,
2692 DiskCliKind::File { path, create_with_len: None, direct: true }
2693 if path == Path::new("/dev/sdb")
2694 ));
2695
2696 let disk = DiskCliKind::from_str("file:disk.img;direct;create=1G").unwrap();
2698 assert!(matches!(
2699 &disk,
2700 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
2701 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
2702 ));
2703
2704 let disk = DiskCliKind::from_str("file:disk.img;create=1G;direct").unwrap();
2705 assert!(matches!(
2706 &disk,
2707 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
2708 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
2709 ));
2710
2711 let disk = DiskCliKind::from_str("file:disk.img").unwrap();
2713 assert!(matches!(
2714 &disk,
2715 DiskCliKind::File { path, create_with_len: None, direct: false }
2716 if path == Path::new("disk.img")
2717 ));
2718
2719 assert!(DiskCliKind::from_str("file:disk.img;bogus").is_err());
2721
2722 assert!(DiskCliKind::from_str("sql:db.sqlite;direct").is_err());
2724 }
2725
2726 #[test]
2727 fn test_parse_memory_disk() {
2728 let s = "mem:1G";
2729 let disk = DiskCliKind::from_str(s).unwrap();
2730 match disk {
2731 DiskCliKind::Memory(size) => {
2732 assert_eq!(size, 1024 * 1024 * 1024); }
2734 _ => panic!("Expected Memory variant"),
2735 }
2736 }
2737
2738 #[test]
2739 fn test_parse_pcie_disk() {
2740 assert_eq!(
2741 DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
2742 Some("p0".to_string())
2743 );
2744 assert_eq!(
2745 DiskCli::from_str("file:path.vhdx,pcie_port=p0")
2746 .unwrap()
2747 .pcie_port,
2748 Some("p0".to_string())
2749 );
2750 assert_eq!(
2751 DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
2752 .unwrap()
2753 .pcie_port,
2754 Some("p0".to_string())
2755 );
2756
2757 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
2759
2760 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
2762 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
2763 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
2764 }
2765
2766 #[test]
2767 fn test_parse_memory_diff_disk() {
2768 let s = "memdiff:file:base.img";
2769 let disk = DiskCliKind::from_str(s).unwrap();
2770 match disk {
2771 DiskCliKind::MemoryDiff(inner) => match *inner {
2772 DiskCliKind::File {
2773 path,
2774 create_with_len,
2775 ..
2776 } => {
2777 assert_eq!(path, PathBuf::from("base.img"));
2778 assert_eq!(create_with_len, None);
2779 }
2780 _ => panic!("Expected File variant inside MemoryDiff"),
2781 },
2782 _ => panic!("Expected MemoryDiff variant"),
2783 }
2784 }
2785
2786 #[test]
2787 fn test_parse_sqlite_disk() {
2788 let s = "sql:db.sqlite;create=2G";
2789 let disk = DiskCliKind::from_str(s).unwrap();
2790 match disk {
2791 DiskCliKind::Sqlite {
2792 path,
2793 create_with_len,
2794 } => {
2795 assert_eq!(path, PathBuf::from("db.sqlite"));
2796 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
2797 }
2798 _ => panic!("Expected Sqlite variant"),
2799 }
2800
2801 let s = "sql:db.sqlite";
2803 let disk = DiskCliKind::from_str(s).unwrap();
2804 match disk {
2805 DiskCliKind::Sqlite {
2806 path,
2807 create_with_len,
2808 } => {
2809 assert_eq!(path, PathBuf::from("db.sqlite"));
2810 assert_eq!(create_with_len, None);
2811 }
2812 _ => panic!("Expected Sqlite variant"),
2813 }
2814 }
2815
2816 #[test]
2817 fn test_parse_sqlite_diff_disk() {
2818 let s = "sqldiff:diff.sqlite;create:file:base.img";
2820 let disk = DiskCliKind::from_str(s).unwrap();
2821 match disk {
2822 DiskCliKind::SqliteDiff { path, create, disk } => {
2823 assert_eq!(path, PathBuf::from("diff.sqlite"));
2824 assert!(create);
2825 match *disk {
2826 DiskCliKind::File {
2827 path,
2828 create_with_len,
2829 ..
2830 } => {
2831 assert_eq!(path, PathBuf::from("base.img"));
2832 assert_eq!(create_with_len, None);
2833 }
2834 _ => panic!("Expected File variant inside SqliteDiff"),
2835 }
2836 }
2837 _ => panic!("Expected SqliteDiff variant"),
2838 }
2839
2840 let s = "sqldiff:diff.sqlite:file:base.img";
2842 let disk = DiskCliKind::from_str(s).unwrap();
2843 match disk {
2844 DiskCliKind::SqliteDiff { path, create, disk } => {
2845 assert_eq!(path, PathBuf::from("diff.sqlite"));
2846 assert!(!create);
2847 match *disk {
2848 DiskCliKind::File {
2849 path,
2850 create_with_len,
2851 ..
2852 } => {
2853 assert_eq!(path, PathBuf::from("base.img"));
2854 assert_eq!(create_with_len, None);
2855 }
2856 _ => panic!("Expected File variant inside SqliteDiff"),
2857 }
2858 }
2859 _ => panic!("Expected SqliteDiff variant"),
2860 }
2861 }
2862
2863 #[test]
2864 fn test_parse_autocache_sqlite_disk() {
2865 let disk =
2867 DiskCliKind::parse_autocache(":file:disk.vhd", Ok("/tmp/cache".to_string())).unwrap();
2868 assert!(matches!(
2869 disk,
2870 DiskCliKind::AutoCacheSqlite {
2871 cache_path,
2872 key,
2873 disk: _disk,
2874 } if cache_path == "/tmp/cache" && key.is_none()
2875 ));
2876
2877 let disk =
2879 DiskCliKind::parse_autocache("mykey:file:disk.vhd", Ok("/tmp/cache".to_string()))
2880 .unwrap();
2881 assert!(matches!(
2882 disk,
2883 DiskCliKind::AutoCacheSqlite {
2884 cache_path,
2885 key: Some(key),
2886 disk: _disk,
2887 } if cache_path == "/tmp/cache" && key == "mykey"
2888 ));
2889
2890 assert!(
2892 DiskCliKind::parse_autocache(":file:disk.vhd", Err(std::env::VarError::NotPresent),)
2893 .is_err()
2894 );
2895 }
2896
2897 #[test]
2898 fn test_parse_disk_errors() {
2899 assert!(DiskCliKind::from_str("invalid:").is_err());
2900 assert!(DiskCliKind::from_str("memory:extra").is_err());
2901
2902 assert!(DiskCliKind::from_str("sqlite:").is_err());
2904 }
2905
2906 #[test]
2907 fn test_parse_errors() {
2908 assert!(DiskCliKind::from_str("mem:invalid").is_err());
2910
2911 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
2913
2914 assert!(
2916 DiskCliKind::parse_autocache("key:file:disk.vhd", Err(std::env::VarError::NotPresent),)
2917 .is_err()
2918 );
2919
2920 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
2922
2923 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
2925
2926 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
2928
2929 assert!(DiskCliKind::from_str("invalid:path").is_err());
2931
2932 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
2934 }
2935
2936 #[test]
2937 fn test_fs_args_from_str() {
2938 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
2939 assert_eq!(args.tag, "tag1");
2940 assert_eq!(args.path, "/path/to/fs");
2941
2942 assert!(FsArgs::from_str("tag1").is_err());
2944 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
2945 }
2946
2947 #[test]
2948 fn test_fs_args_with_options_from_str() {
2949 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
2950 assert_eq!(args.tag, "tag1");
2951 assert_eq!(args.path, "/path/to/fs");
2952 assert_eq!(args.options, "opt1;opt2");
2953
2954 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
2956 assert_eq!(args.tag, "tag1");
2957 assert_eq!(args.path, "/path/to/fs");
2958 assert_eq!(args.options, "");
2959
2960 assert!(FsArgsWithOptions::from_str("tag1").is_err());
2962 }
2963
2964 #[test]
2965 fn test_serial_config_from_str() {
2966 assert_eq!(
2967 SerialConfigCli::from_str("none").unwrap(),
2968 SerialConfigCli::None
2969 );
2970 assert_eq!(
2971 SerialConfigCli::from_str("console").unwrap(),
2972 SerialConfigCli::Console
2973 );
2974 assert_eq!(
2975 SerialConfigCli::from_str("stderr").unwrap(),
2976 SerialConfigCli::Stderr
2977 );
2978
2979 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
2981 if let SerialConfigCli::File(path) = file_config {
2982 assert_eq!(path.to_str().unwrap(), "/path/to/file");
2983 } else {
2984 panic!("Expected File variant");
2985 }
2986
2987 match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
2989 SerialConfigCli::NewConsole(None, Some(name)) => {
2990 assert_eq!(name, "MyTerm");
2991 }
2992 _ => panic!("Expected NewConsole variant with name"),
2993 }
2994
2995 match SerialConfigCli::from_str("term").unwrap() {
2997 SerialConfigCli::NewConsole(None, None) => (),
2998 _ => panic!("Expected NewConsole variant without name"),
2999 }
3000
3001 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
3003 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
3004 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
3005 assert_eq!(name, "MyTerm");
3006 }
3007 _ => panic!("Expected NewConsole variant with name"),
3008 }
3009
3010 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
3012 SerialConfigCli::NewConsole(Some(path), None) => {
3013 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
3014 }
3015 _ => panic!("Expected NewConsole variant without name"),
3016 }
3017
3018 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
3020 SerialConfigCli::Tcp(addr) => {
3021 assert_eq!(addr.to_string(), "127.0.0.1:1234");
3022 }
3023 _ => panic!("Expected Tcp variant"),
3024 }
3025
3026 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
3028 SerialConfigCli::Pipe(path) => {
3029 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
3030 }
3031 _ => panic!("Expected Pipe variant"),
3032 }
3033
3034 assert!(SerialConfigCli::from_str("").is_err());
3036 assert!(SerialConfigCli::from_str("unknown").is_err());
3037 assert!(SerialConfigCli::from_str("file").is_err());
3038 assert!(SerialConfigCli::from_str("listen").is_err());
3039 }
3040
3041 #[test]
3042 fn test_endpoint_config_from_str() {
3043 assert!(matches!(
3045 EndpointConfigCli::from_str("none").unwrap(),
3046 EndpointConfigCli::None
3047 ));
3048
3049 match EndpointConfigCli::from_str("consomme").unwrap() {
3051 EndpointConfigCli::Consomme {
3052 cidr: None,
3053 host_fwd,
3054 } => assert!(host_fwd.is_empty()),
3055 _ => panic!("Expected Consomme variant without cidr"),
3056 }
3057
3058 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
3060 EndpointConfigCli::Consomme {
3061 cidr: Some(cidr),
3062 host_fwd,
3063 } => {
3064 assert_eq!(cidr, "192.168.0.0/24");
3065 assert!(host_fwd.is_empty());
3066 }
3067 _ => panic!("Expected Consomme variant with cidr"),
3068 }
3069
3070 match EndpointConfigCli::from_str("consomme:hostfwd=udp:127.0.0.1:5000-:5000").unwrap() {
3072 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3073 assert!(cidr.is_none());
3074 assert_eq!(host_fwd.len(), 1);
3075 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Udp);
3076 assert_eq!(
3077 host_fwd[0].host_address,
3078 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
3079 );
3080 assert_eq!(host_fwd[0].host_port, 5000);
3081 assert_eq!(host_fwd[0].guest_port, 5000);
3082 }
3083 _ => panic!("Expected Consomme variant with hostfwd"),
3084 }
3085
3086 match EndpointConfigCli::from_str("consomme:10.0.0.0/24,hostfwd=tcp::2222-:22").unwrap() {
3088 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3089 assert_eq!(cidr.as_deref(), Some("10.0.0.0/24"));
3090 assert_eq!(host_fwd.len(), 1);
3091 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3092 assert_eq!(host_fwd[0].host_port, 2222);
3093 assert_eq!(host_fwd[0].guest_port, 22);
3094 }
3095 _ => panic!("Expected Consomme variant with cidr and hostfwd"),
3096 }
3097
3098 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::2222-:22,hostfwd=tcp::3389-:3389")
3100 .unwrap()
3101 {
3102 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3103 assert!(cidr.is_none());
3104 assert_eq!(host_fwd.len(), 2);
3105 assert_eq!(host_fwd[0].host_port, 2222);
3106 assert_eq!(host_fwd[0].guest_port, 22);
3107 assert_eq!(host_fwd[1].host_port, 3389);
3108 assert_eq!(host_fwd[1].guest_port, 3389);
3109 }
3110 _ => panic!("Expected Consomme variant with multiple hostfwd"),
3111 }
3112
3113 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:127.0.0.1:8080-:80").unwrap() {
3115 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3116 assert!(cidr.is_none());
3117 assert_eq!(host_fwd.len(), 1);
3118 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3119 assert_eq!(
3120 host_fwd[0].host_address,
3121 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
3122 );
3123 assert_eq!(host_fwd[0].host_port, 8080);
3124 assert_eq!(host_fwd[0].guest_port, 80);
3125 }
3126 _ => panic!("Expected Consomme variant with host/guest port mapping"),
3127 }
3128
3129 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-10.0.0.2:80").unwrap() {
3131 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3132 assert!(cidr.is_none());
3133 assert_eq!(host_fwd[0].host_port, 8080);
3134 assert_eq!(host_fwd[0].guest_port, 80);
3135 }
3136 _ => panic!("Expected Consomme variant with guest address"),
3137 }
3138
3139 match EndpointConfigCli::from_str("consomme:hostfwd=tcp:[::1]:8080-:80").unwrap() {
3141 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3142 assert!(cidr.is_none());
3143 assert_eq!(host_fwd.len(), 1);
3144 assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3145 assert_eq!(
3146 host_fwd[0].host_address,
3147 Some(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST))
3148 );
3149 assert_eq!(host_fwd[0].host_port, 8080);
3150 assert_eq!(host_fwd[0].guest_port, 80);
3151 }
3152 _ => panic!("Expected Consomme variant with IPv6 hostfwd"),
3153 }
3154
3155 match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-[::1]:80").unwrap() {
3157 EndpointConfigCli::Consomme { cidr, host_fwd } => {
3158 assert!(cidr.is_none());
3159 assert_eq!(host_fwd[0].host_port, 8080);
3160 assert_eq!(host_fwd[0].guest_port, 80);
3161 }
3162 _ => panic!("Expected Consomme variant with IPv6 guest address"),
3163 }
3164
3165 match EndpointConfigCli::from_str("dio").unwrap() {
3167 EndpointConfigCli::Dio { id: None } => (),
3168 _ => panic!("Expected Dio variant without id"),
3169 }
3170
3171 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
3173 EndpointConfigCli::Dio { id: Some(id) } => {
3174 assert_eq!(id, "test_id");
3175 }
3176 _ => panic!("Expected Dio variant with id"),
3177 }
3178
3179 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
3181 EndpointConfigCli::Tap { name } => {
3182 assert_eq!(name, "tap0");
3183 }
3184 _ => panic!("Expected Tap variant"),
3185 }
3186
3187 assert!(EndpointConfigCli::from_str("invalid").is_err());
3189 }
3190
3191 #[test]
3192 fn test_nic_config_from_str() {
3193 use openvmm_defs::config::DeviceVtl;
3194
3195 let config = NicConfigCli::from_str("none").unwrap();
3197 assert_eq!(config.vtl, DeviceVtl::Vtl0);
3198 assert!(config.max_queues.is_none());
3199 assert!(!config.underhill);
3200 assert!(config.pcie_port.is_none());
3201 assert!(matches!(config.endpoint, EndpointConfigCli::None));
3202
3203 let config = NicConfigCli::from_str("vtl2:none").unwrap();
3205 assert_eq!(config.vtl, DeviceVtl::Vtl2);
3206 assert!(config.pcie_port.is_none());
3207 assert!(matches!(config.endpoint, EndpointConfigCli::None));
3208
3209 let config = NicConfigCli::from_str("queues=4:none").unwrap();
3211 assert_eq!(config.max_queues, Some(4));
3212 assert!(config.pcie_port.is_none());
3213 assert!(matches!(config.endpoint, EndpointConfigCli::None));
3214
3215 let config = NicConfigCli::from_str("uh:none").unwrap();
3217 assert!(config.underhill);
3218 assert!(config.pcie_port.is_none());
3219 assert!(matches!(config.endpoint, EndpointConfigCli::None));
3220
3221 let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
3223 assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
3224 assert!(matches!(config.endpoint, EndpointConfigCli::None));
3225
3226 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
3228 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
3230 assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
3231 assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
3232 assert!(NicConfigCli::from_str("pcie_port:none").is_err());
3233 }
3234
3235 #[test]
3236 fn test_parse_pcie_port_prefix() {
3237 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
3239 assert_eq!(port.unwrap(), "rp0");
3240 assert_eq!(rest, "tag,path");
3241
3242 let (port, rest) = parse_pcie_port_prefix("tag,path");
3244 assert!(port.is_none());
3245 assert_eq!(rest, "tag,path");
3246
3247 let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
3249 assert!(port.is_none());
3250 assert_eq!(rest, "pcie_port=:tag,path");
3251
3252 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
3254 assert!(port.is_none());
3255 assert_eq!(rest, "pcie_port=rp0");
3256 }
3257
3258 #[test]
3259 fn test_fs_args_pcie_port() {
3260 let args = FsArgs::from_str("myfs,/path").unwrap();
3262 assert_eq!(args.tag, "myfs");
3263 assert_eq!(args.path, "/path");
3264 assert!(args.pcie_port.is_none());
3265
3266 let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
3268 assert_eq!(args.pcie_port.unwrap(), "rp0");
3269 assert_eq!(args.tag, "myfs");
3270 assert_eq!(args.path, "/path");
3271
3272 assert!(FsArgs::from_str("myfs").is_err());
3274 assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
3275 }
3276
3277 #[test]
3278 fn test_fs_args_with_options_pcie_port() {
3279 let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
3281 assert_eq!(args.tag, "myfs");
3282 assert_eq!(args.path, "/path");
3283 assert_eq!(args.options, "uid=1000");
3284 assert!(args.pcie_port.is_none());
3285
3286 let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
3288 assert_eq!(args.pcie_port.unwrap(), "rp0");
3289 assert_eq!(args.tag, "myfs");
3290 assert_eq!(args.path, "/path");
3291 assert_eq!(args.options, "uid=1000");
3292
3293 assert!(FsArgsWithOptions::from_str("myfs").is_err());
3295 }
3296
3297 #[test]
3298 fn test_virtio_pmem_args_pcie_port() {
3299 let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
3301 assert_eq!(args.path, "/path/to/file");
3302 assert!(args.pcie_port.is_none());
3303
3304 let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
3306 assert_eq!(args.pcie_port.unwrap(), "rp0");
3307 assert_eq!(args.path, "/path/to/file");
3308
3309 assert!(VirtioPmemArgs::from_str("").is_err());
3311 assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
3312 }
3313
3314 #[test]
3315 fn test_smt_config_from_str() {
3316 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
3317 assert_eq!(
3318 SmtConfigCli::from_str("force").unwrap(),
3319 SmtConfigCli::Force
3320 );
3321 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
3322
3323 assert!(SmtConfigCli::from_str("invalid").is_err());
3325 assert!(SmtConfigCli::from_str("").is_err());
3326 }
3327
3328 #[test]
3329 fn test_pcat_boot_order_from_str() {
3330 let order = PcatBootOrderCli::from_str("optical").unwrap();
3332 assert_eq!(order.0[0], PcatBootDevice::Optical);
3333
3334 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
3336 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
3337 assert_eq!(order.0[1], PcatBootDevice::Network);
3338
3339 assert!(PcatBootOrderCli::from_str("invalid").is_err());
3341 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
3343
3344 #[test]
3345 fn test_floppy_disk_from_str() {
3346 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
3348 assert!(!disk.read_only);
3349 match disk.kind {
3350 DiskCliKind::File {
3351 path,
3352 create_with_len,
3353 ..
3354 } => {
3355 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
3356 assert_eq!(create_with_len, None);
3357 }
3358 _ => panic!("Expected File variant"),
3359 }
3360
3361 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
3363 assert!(disk.read_only);
3364
3365 assert!(FloppyDiskCli::from_str("").is_err());
3367 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
3368 }
3369
3370 #[test]
3371 fn test_pcie_root_complex_from_str() {
3372 const ONE_MB: u64 = 1024 * 1024;
3373 const ONE_GB: u64 = 1024 * ONE_MB;
3374
3375 const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
3376 const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
3377
3378 assert_eq!(
3379 PcieRootComplexCli::from_str("rc0").unwrap(),
3380 PcieRootComplexCli {
3381 name: "rc0".to_string(),
3382 segment: 0,
3383 start_bus: 0,
3384 end_bus: 255,
3385 low_mmio: DEFAULT_LOW_MMIO,
3386 high_mmio: DEFAULT_HIGH_MMIO,
3387 }
3388 );
3389
3390 assert_eq!(
3391 PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
3392 PcieRootComplexCli {
3393 name: "rc1".to_string(),
3394 segment: 1,
3395 start_bus: 0,
3396 end_bus: 255,
3397 low_mmio: DEFAULT_LOW_MMIO,
3398 high_mmio: DEFAULT_HIGH_MMIO,
3399 }
3400 );
3401
3402 assert_eq!(
3403 PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
3404 PcieRootComplexCli {
3405 name: "rc2".to_string(),
3406 segment: 0,
3407 start_bus: 32,
3408 end_bus: 255,
3409 low_mmio: DEFAULT_LOW_MMIO,
3410 high_mmio: DEFAULT_HIGH_MMIO,
3411 }
3412 );
3413
3414 assert_eq!(
3415 PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
3416 PcieRootComplexCli {
3417 name: "rc3".to_string(),
3418 segment: 0,
3419 start_bus: 0,
3420 end_bus: 31,
3421 low_mmio: DEFAULT_LOW_MMIO,
3422 high_mmio: DEFAULT_HIGH_MMIO,
3423 }
3424 );
3425
3426 assert_eq!(
3427 PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
3428 PcieRootComplexCli {
3429 name: "rc4".to_string(),
3430 segment: 0,
3431 start_bus: 32,
3432 end_bus: 127,
3433 low_mmio: DEFAULT_LOW_MMIO,
3434 high_mmio: 2 * ONE_GB,
3435 }
3436 );
3437
3438 assert_eq!(
3439 PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
3440 PcieRootComplexCli {
3441 name: "rc5".to_string(),
3442 segment: 2,
3443 start_bus: 32,
3444 end_bus: 127,
3445 low_mmio: DEFAULT_LOW_MMIO,
3446 high_mmio: DEFAULT_HIGH_MMIO,
3447 }
3448 );
3449
3450 assert_eq!(
3451 PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
3452 PcieRootComplexCli {
3453 name: "rc6".to_string(),
3454 segment: 0,
3455 start_bus: 0,
3456 end_bus: 255,
3457 low_mmio: ONE_MB as u32,
3458 high_mmio: 64 * ONE_GB,
3459 }
3460 );
3461
3462 assert!(PcieRootComplexCli::from_str("").is_err());
3464 assert!(PcieRootComplexCli::from_str("poorly,").is_err());
3465 assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
3466 assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
3467 assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
3468 assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
3469 assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
3470 assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
3471 assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
3472 assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
3473 assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
3474 }
3475
3476 #[test]
3477 fn test_pcie_root_port_from_str() {
3478 assert_eq!(
3479 PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
3480 PcieRootPortCli {
3481 root_complex_name: "rc0".to_string(),
3482 name: "rc0rp0".to_string(),
3483 hotplug: false,
3484 acs_capabilities_supported: None,
3485 }
3486 );
3487
3488 assert_eq!(
3489 PcieRootPortCli::from_str("my_rc:port2").unwrap(),
3490 PcieRootPortCli {
3491 root_complex_name: "my_rc".to_string(),
3492 name: "port2".to_string(),
3493 hotplug: false,
3494 acs_capabilities_supported: None,
3495 }
3496 );
3497
3498 assert_eq!(
3500 PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
3501 PcieRootPortCli {
3502 root_complex_name: "my_rc".to_string(),
3503 name: "port2".to_string(),
3504 hotplug: true,
3505 acs_capabilities_supported: None,
3506 }
3507 );
3508
3509 assert_eq!(
3510 PcieRootPortCli::from_str("my_rc:port3,acs=0").unwrap(),
3511 PcieRootPortCli {
3512 root_complex_name: "my_rc".to_string(),
3513 name: "port3".to_string(),
3514 hotplug: false,
3515 acs_capabilities_supported: Some(0),
3516 }
3517 );
3518
3519 assert_eq!(
3520 PcieRootPortCli::from_str("my_rc:port3,acs=0x5f").unwrap(),
3521 PcieRootPortCli {
3522 root_complex_name: "my_rc".to_string(),
3523 name: "port3".to_string(),
3524 hotplug: false,
3525 acs_capabilities_supported: Some(0x005f),
3526 }
3527 );
3528
3529 assert!(PcieRootPortCli::from_str("").is_err());
3531 assert!(PcieRootPortCli::from_str("rp0").is_err());
3532 assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
3533 assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
3534 assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
3535 }
3536
3537 #[test]
3538 fn test_pcie_switch_from_str() {
3539 assert_eq!(
3540 GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
3541 GenericPcieSwitchCli {
3542 port_name: "rp0".to_string(),
3543 name: "switch0".to_string(),
3544 num_downstream_ports: 4,
3545 hotplug: false,
3546 acs_capabilities_supported: None,
3547 }
3548 );
3549
3550 assert_eq!(
3551 GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
3552 GenericPcieSwitchCli {
3553 port_name: "port1".to_string(),
3554 name: "my_switch".to_string(),
3555 num_downstream_ports: 4,
3556 hotplug: false,
3557 acs_capabilities_supported: None,
3558 }
3559 );
3560
3561 assert_eq!(
3562 GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
3563 GenericPcieSwitchCli {
3564 port_name: "rp2".to_string(),
3565 name: "sw".to_string(),
3566 num_downstream_ports: 8,
3567 hotplug: false,
3568 acs_capabilities_supported: None,
3569 }
3570 );
3571
3572 assert_eq!(
3574 GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
3575 GenericPcieSwitchCli {
3576 port_name: "switch0-downstream-1".to_string(),
3577 name: "child_switch".to_string(),
3578 num_downstream_ports: 4,
3579 hotplug: false,
3580 acs_capabilities_supported: None,
3581 }
3582 );
3583
3584 assert_eq!(
3586 GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
3587 GenericPcieSwitchCli {
3588 port_name: "rp0".to_string(),
3589 name: "switch0".to_string(),
3590 num_downstream_ports: 4,
3591 hotplug: true,
3592 acs_capabilities_supported: None,
3593 }
3594 );
3595
3596 assert_eq!(
3598 GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
3599 GenericPcieSwitchCli {
3600 port_name: "rp0".to_string(),
3601 name: "switch0".to_string(),
3602 num_downstream_ports: 8,
3603 hotplug: true,
3604 acs_capabilities_supported: None,
3605 }
3606 );
3607
3608 assert_eq!(
3609 GenericPcieSwitchCli::from_str("rp0:switch0,acs=0").unwrap(),
3610 GenericPcieSwitchCli {
3611 port_name: "rp0".to_string(),
3612 name: "switch0".to_string(),
3613 num_downstream_ports: 4,
3614 hotplug: false,
3615 acs_capabilities_supported: Some(0),
3616 }
3617 );
3618
3619 assert_eq!(
3620 GenericPcieSwitchCli::from_str("rp0:switch0,acs=95").unwrap(),
3621 GenericPcieSwitchCli {
3622 port_name: "rp0".to_string(),
3623 name: "switch0".to_string(),
3624 num_downstream_ports: 4,
3625 hotplug: false,
3626 acs_capabilities_supported: Some(95),
3627 }
3628 );
3629
3630 assert!(GenericPcieSwitchCli::from_str("").is_err());
3632 assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
3633 assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
3634 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
3635 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
3636 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
3637 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
3638 }
3639
3640 #[test]
3641 fn test_pcie_remote_from_str() {
3642 assert_eq!(
3644 PcieRemoteCli::from_str("rc0rp0").unwrap(),
3645 PcieRemoteCli {
3646 port_name: "rc0rp0".to_string(),
3647 socket_addr: None,
3648 hu: 0,
3649 controller: 0,
3650 }
3651 );
3652
3653 assert_eq!(
3655 PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
3656 PcieRemoteCli {
3657 port_name: "rc0rp0".to_string(),
3658 socket_addr: Some("localhost:22567".to_string()),
3659 hu: 0,
3660 controller: 0,
3661 }
3662 );
3663
3664 assert_eq!(
3666 PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
3667 PcieRemoteCli {
3668 port_name: "myport".to_string(),
3669 socket_addr: Some("localhost:22568".to_string()),
3670 hu: 1,
3671 controller: 2,
3672 }
3673 );
3674
3675 assert_eq!(
3677 PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
3678 PcieRemoteCli {
3679 port_name: "port0".to_string(),
3680 socket_addr: None,
3681 hu: 5,
3682 controller: 3,
3683 }
3684 );
3685
3686 assert!(PcieRemoteCli::from_str("").is_err());
3688 assert!(PcieRemoteCli::from_str("port,socket=").is_err());
3689 assert!(PcieRemoteCli::from_str("port,hu=").is_err());
3690 assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
3691 assert!(PcieRemoteCli::from_str("port,controller=").is_err());
3692 assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
3693 assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
3694 }
3695
3696 #[test]
3697 fn test_parse_memory_units() {
3698 assert_eq!(parse_memory("64G").unwrap(), 64 * 1024 * 1024 * 1024);
3699 assert_eq!(parse_memory("64GB").unwrap(), 64 * 1024 * 1024 * 1024);
3700 assert_eq!(parse_memory("3MB").unwrap(), 3 * 1024 * 1024);
3701 assert_eq!(parse_memory("512KB").unwrap(), 512 * 1024);
3702 assert!(parse_memory("3MiB").is_err());
3703 }
3704
3705 #[test]
3706 fn test_memory_config_size_only() {
3707 assert_eq!(
3708 parse_memory_config("64G").unwrap(),
3709 MemoryCli {
3710 mem_size: 64 * 1024 * 1024 * 1024,
3711 shared: None,
3712 prefetch: false,
3713 transparent_hugepages: false,
3714 hugepages: false,
3715 hugepage_size: None,
3716 file: None,
3717 }
3718 );
3719 }
3720
3721 #[test]
3722 fn test_memory_config_key_value() {
3723 assert_eq!(
3724 parse_memory_config("size=2G,shared=off,prefetch=on,thp=on").unwrap(),
3725 MemoryCli {
3726 mem_size: 2 * 1024 * 1024 * 1024,
3727 shared: Some(false),
3728 prefetch: true,
3729 transparent_hugepages: true,
3730 hugepages: false,
3731 hugepage_size: None,
3732 file: None,
3733 }
3734 );
3735
3736 assert_eq!(
3737 parse_memory_config("size=4GB,hugepages=on,hugepage_size=2MB").unwrap(),
3738 MemoryCli {
3739 mem_size: 4 * 1024 * 1024 * 1024,
3740 shared: None,
3741 prefetch: false,
3742 transparent_hugepages: false,
3743 hugepages: true,
3744 hugepage_size: Some(2 * 1024 * 1024),
3745 file: None,
3746 }
3747 );
3748
3749 assert_eq!(
3750 parse_memory_config("file=/tmp/memory.bin").unwrap(),
3751 MemoryCli {
3752 mem_size: DEFAULT_MEMORY_SIZE,
3753 shared: None,
3754 prefetch: false,
3755 transparent_hugepages: false,
3756 hugepages: false,
3757 hugepage_size: None,
3758 file: Some(PathBuf::from("/tmp/memory.bin")),
3759 }
3760 );
3761 }
3762
3763 #[test]
3764 fn test_memory_config_rejects_invalid_combinations() {
3765 assert!(parse_memory_config("thp=on").is_err());
3766 assert!(parse_memory_config("size=1G,size=2G").is_err());
3767 assert!(parse_memory_config("hugepage_size=2M").is_err());
3768 assert!(parse_memory_config("hugepages=on,shared=off").is_err());
3769 assert!(parse_memory_config("hugepages=on,file=/tmp/memory.bin").is_err());
3770
3771 assert_eq!(
3774 parse_memory_config("hugepages=on,hugepage_size=3MB")
3775 .unwrap()
3776 .hugepage_size,
3777 Some(3 * 1024 * 1024)
3778 );
3779 }
3780
3781 #[test]
3782 fn test_memory_options_merge_legacy_aliases() {
3783 let opt = Options::try_parse_from([
3784 "openvmm",
3785 "--memory",
3786 "2G",
3787 "--prefetch",
3788 "--private-memory",
3789 "--thp",
3790 ])
3791 .unwrap();
3792 opt.validate_memory_options().unwrap();
3793 assert_eq!(opt.memory_size(), 2 * 1024 * 1024 * 1024);
3794 assert!(opt.prefetch_memory());
3795 assert!(opt.private_memory());
3796 assert!(opt.transparent_hugepages());
3797 }
3798
3799 #[test]
3800 fn test_memory_options_allow_legacy_thp_with_new_private_memory() {
3801 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=off", "--thp"]).unwrap();
3802 opt.validate_memory_options().unwrap();
3803 assert!(opt.private_memory());
3804 assert!(opt.transparent_hugepages());
3805 }
3806
3807 #[test]
3808 fn test_memory_options_reject_conflicting_legacy_aliases() {
3809 let opt = Options::try_parse_from(["openvmm", "--memory", "shared=on", "--private-memory"])
3810 .unwrap();
3811 assert!(opt.validate_memory_options().is_err());
3812 }
3813
3814 #[test]
3815 fn test_memory_options_reject_hugepage_legacy_conflicts() {
3816 let opt =
3817 Options::try_parse_from(["openvmm", "--memory", "hugepages=on", "--private-memory"])
3818 .unwrap();
3819 assert!(opt.validate_memory_options().is_err());
3820
3821 let opt = Options::try_parse_from([
3822 "openvmm",
3823 "--memory",
3824 "hugepages=on",
3825 "--memory-backing-file",
3826 "/tmp/memory.bin",
3827 ])
3828 .unwrap();
3829 assert!(opt.validate_memory_options().is_err());
3830 }
3831
3832 #[test]
3833 fn test_pidfile_option_parsed() {
3834 let opt = Options::try_parse_from(["openvmm", "--pidfile", "/tmp/test.pid"]).unwrap();
3835 assert_eq!(opt.pidfile, Some(PathBuf::from("/tmp/test.pid")));
3836 }
3837}