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
35#[derive(Parser)]
40pub struct Options {
41 #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
43 pub processors: u32,
44
45 #[clap(
47 short = 'm',
48 long,
49 value_name = "SIZE",
50 default_value = "1GB",
51 value_parser = parse_memory
52 )]
53 pub memory: u64,
54
55 #[clap(short = 'M', long)]
57 pub shared_memory: bool,
58
59 #[clap(long)]
61 pub prefetch: bool,
62
63 #[clap(long, value_name = "FILE", conflicts_with = "private_memory")]
67 pub memory_backing_file: Option<PathBuf>,
68
69 #[clap(long, value_name = "DIR", conflicts_with = "memory_backing_file")]
72 pub restore_snapshot: Option<PathBuf>,
73
74 #[clap(long, conflicts_with_all = ["memory_backing_file", "restore_snapshot"])]
76 pub private_memory: bool,
77
78 #[clap(long, requires("private_memory"))]
80 pub thp: bool,
81
82 #[clap(short = 'P', long)]
84 pub paused: bool,
85
86 #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
88 pub kernel: OptionalPathBuf,
89
90 #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
92 pub initrd: OptionalPathBuf,
93
94 #[clap(short = 'c', long, value_name = "STRING")]
96 pub cmdline: Vec<String>,
97
98 #[clap(long)]
100 pub hv: bool,
101
102 #[clap(long, conflicts_with_all = ["uefi", "pcat", "igvm"])]
106 pub device_tree: bool,
107
108 #[clap(long, requires("hv"))]
112 pub vtl2: bool,
113
114 #[clap(long, requires("hv"))]
117 pub get: bool,
118
119 #[clap(long, conflicts_with("get"))]
122 pub no_get: bool,
123
124 #[clap(long, requires("vtl2"))]
126 pub no_alias_map: bool,
127
128 #[clap(long, requires("vtl2"))]
130 pub isolation: Option<IsolationCli>,
131
132 #[clap(long, value_name = "PATH", alias = "vsock-path")]
134 pub vmbus_vsock_path: Option<String>,
135
136 #[clap(long, value_name = "PATH", requires("vtl2"), alias = "vtl2-vsock-path")]
138 pub vmbus_vtl2_vsock_path: Option<String>,
139
140 #[clap(long, requires("vtl2"), default_value = "halt")]
142 pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
143
144 #[clap(long_help = r#"
146e.g: --disk memdiff:file:/path/to/disk.vhd
147
148syntax: <path> | kind:<arg>[,flag,opt=arg,...]
149
150valid disk kinds:
151 `mem:<len>` memory backed disk
152 <len>: length of ramdisk, e.g.: `1G`
153 `memdiff:<disk>` memory backed diff disk
154 <disk>: lower disk, e.g.: `file:base.img`
155 `file:<path>[;direct][;create=<len>]` file-backed disk
156 <path>: path to file
157 `;direct`: bypass the OS page cache
158 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
159 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
160 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
161 `blob:<type>:<url>` HTTP blob (read-only)
162 <type>: `flat` or `vhd1`
163 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
164 <cipher>: `xts-aes-256`
165 `prwrap:<disk>` persistent reservations wrapper
166
167flags:
168 `ro` open disk as read-only
169 `dvd` specifies that device is cd/dvd and it is read_only
170 `vtl2` assign this disk to VTL2
171 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
172 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
173
174options:
175 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
176"#)]
177 #[clap(long, value_name = "FILE")]
178 pub disk: Vec<DiskCli>,
179
180 #[clap(long_help = r#"
182e.g: --nvme memdiff:file:/path/to/disk.vhd
183
184syntax: <path> | kind:<arg>[,flag,opt=arg,...]
185
186valid disk kinds:
187 `mem:<len>` memory backed disk
188 <len>: length of ramdisk, e.g.: `1G`
189 `memdiff:<disk>` memory backed diff disk
190 <disk>: lower disk, e.g.: `file:base.img`
191 `file:<path>[;direct][;create=<len>]` file-backed disk
192 <path>: path to file
193 `;direct`: bypass the OS page cache
194 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
195 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
196 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
197 `blob:<type>:<url>` HTTP blob (read-only)
198 <type>: `flat` or `vhd1`
199 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
200 <cipher>: `xts-aes-256`
201 `prwrap:<disk>` persistent reservations wrapper
202
203flags:
204 `ro` open disk as read-only
205 `vtl2` assign this disk to VTL2
206 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
207 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
208
209options:
210 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
211"#)]
212 #[clap(long)]
213 pub nvme: Vec<DiskCli>,
214
215 #[clap(long_help = r#"
217e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
218
219syntax: <path> | kind:<arg>[,flag,opt=arg,...]
220
221valid disk kinds:
222 `mem:<len>` memory backed disk
223 <len>: length of ramdisk, e.g.: `1G`
224 `memdiff:<disk>` memory backed diff disk
225 <disk>: lower disk, e.g.: `file:base.img`
226 `file:<path>[;direct]` file-backed disk
227 <path>: path to file
228 `;direct`: bypass the OS page cache
229
230flags:
231 `ro` open disk as read-only
232
233options:
234 `pcie_port=<name>` present the disk using pcie under the specified port
235"#)]
236 #[clap(long = "virtio-blk")]
237 pub virtio_blk: Vec<DiskCli>,
238
239 #[cfg(target_os = "linux")]
264 #[clap(long = "vhost-user")]
265 pub vhost_user: Vec<VhostUserCli>,
266
267 #[clap(long, value_name = "COUNT", default_value = "0")]
269 pub scsi_sub_channels: u16,
270
271 #[clap(long)]
273 pub nic: bool,
274
275 #[clap(long)]
281 pub net: Vec<NicConfigCli>,
282
283 #[clap(long, value_name = "SWITCH_ID")]
287 pub kernel_vmnic: Vec<String>,
288
289 #[clap(long)]
291 pub gfx: bool,
292
293 #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
295 pub vtl2_gfx: bool,
296
297 #[clap(long)]
299 pub vnc: bool,
300
301 #[clap(long, value_name = "PORT", default_value = "5900")]
303 pub vnc_port: u16,
304
305 #[cfg(guest_arch = "x86_64")]
307 #[clap(long, default_value_t)]
308 pub apic_id_offset: u32,
309
310 #[clap(long)]
312 pub vps_per_socket: Option<u32>,
313
314 #[clap(long, default_value = "auto")]
316 pub smt: SmtConfigCli,
317
318 #[cfg(guest_arch = "x86_64")]
320 #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
321 pub x2apic: X2ApicConfig,
322
323 #[clap(long, value_name = "SERIAL")]
325 pub com1: Option<SerialConfigCli>,
326
327 #[clap(long, value_name = "SERIAL")]
329 pub com2: Option<SerialConfigCli>,
330
331 #[clap(long, value_name = "SERIAL")]
333 pub com3: Option<SerialConfigCli>,
334
335 #[clap(long, value_name = "SERIAL")]
337 pub com4: Option<SerialConfigCli>,
338
339 #[structopt(long, value_name = "SERIAL")]
341 pub vmbus_com1_serial: Option<SerialConfigCli>,
342
343 #[structopt(long, value_name = "SERIAL")]
345 pub vmbus_com2_serial: Option<SerialConfigCli>,
346
347 #[clap(long)]
349 pub serial_tx_only: bool,
350
351 #[clap(long, value_name = "SERIAL")]
353 pub debugcon: Option<DebugconSerialConfigCli>,
354
355 #[clap(long, short = 'e')]
357 pub uefi: bool,
358
359 #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
361 pub uefi_firmware: OptionalPathBuf,
362
363 #[clap(long, requires("uefi"))]
365 pub uefi_debug: bool,
366
367 #[clap(long, requires("uefi"))]
369 pub uefi_enable_memory_protections: bool,
370
371 #[clap(long, requires("pcat"))]
382 pub pcat_boot_order: Option<PcatBootOrderCli>,
383
384 #[clap(long, conflicts_with("uefi"))]
386 pub pcat: bool,
387
388 #[clap(long, requires("pcat"), value_name = "FILE")]
390 pub pcat_firmware: Option<PathBuf>,
391
392 #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
394 pub igvm: Option<PathBuf>,
395
396 #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
399 pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
400
401 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
406 pub virtio_9p: Vec<FsArgs>,
407
408 #[clap(long)]
410 pub virtio_9p_debug: bool,
411
412 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
417 pub virtio_fs: Vec<FsArgsWithOptions>,
418
419 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
424 pub virtio_fs_shmem: Vec<FsArgs>,
425
426 #[clap(long, value_name = "BUS", default_value = "auto")]
428 pub virtio_fs_bus: VirtioBusCli,
429
430 #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
435 pub virtio_pmem: Option<VirtioPmemArgs>,
436
437 #[clap(long)]
439 pub virtio_rng: bool,
440
441 #[clap(long, value_name = "BUS", default_value = "auto")]
443 pub virtio_rng_bus: VirtioBusCli,
444
445 #[clap(long, value_name = "PORT", requires("virtio_rng"))]
447 pub virtio_rng_pcie_port: Option<String>,
448
449 #[clap(long)]
455 pub virtio_console: Option<SerialConfigCli>,
456
457 #[clap(long, value_name = "PORT", requires("virtio_console"))]
459 pub virtio_console_pcie_port: Option<String>,
460
461 #[clap(long, value_name = "PATH")]
463 pub virtio_vsock_path: Option<String>,
464
465 #[clap(long)]
472 pub virtio_net: Vec<NicConfigCli>,
473
474 #[clap(long, value_name = "PATH")]
476 pub log_file: Option<PathBuf>,
477
478 #[clap(long, value_name = "PATH")]
482 pub pidfile: Option<PathBuf>,
483
484 #[clap(long, value_name = "SOCKETPATH")]
486 pub ttrpc: Option<PathBuf>,
487
488 #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
490 pub grpc: Option<PathBuf>,
491
492 #[clap(long)]
494 pub single_process: bool,
495
496 #[cfg(windows)]
498 #[clap(long, value_name = "PATH")]
499 pub device: Vec<String>,
500
501 #[clap(long, requires("uefi"))]
503 pub disable_frontpage: bool,
504
505 #[clap(long)]
507 pub tpm: bool,
508
509 #[clap(long, default_value = "control", hide(true))]
513 #[expect(clippy::option_option)]
514 pub internal_worker: Option<Option<String>>,
515
516 #[clap(long, requires("vtl2"))]
518 pub vmbus_redirect: bool,
519
520 #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
522 pub vmbus_max_version: Option<u32>,
523
524 #[clap(long_help = r#"
528e.g: --vmgs memdiff:file:/path/to/file.vmgs
529
530syntax: <path> | kind:<arg>[,flag]
531
532valid disk kinds:
533 `mem:<len>` memory backed disk
534 <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
535 `memdiff:<disk>[;create=<len>]` memory backed diff disk
536 <disk>: lower disk, e.g.: `file:base.img`
537 `file:<path>` file-backed disk
538 <path>: path to file
539
540flags:
541 `fmt` reprovision the VMGS before boot
542 `fmt-on-fail` reprovision the VMGS before boot if it is corrupted
543"#)]
544 #[clap(long)]
545 pub vmgs: Option<VmgsCli>,
546
547 #[clap(long, requires("vmgs"))]
549 pub test_gsp_by_id: bool,
550
551 #[clap(long, requires("pcat"), value_name = "FILE")]
553 pub vga_firmware: Option<PathBuf>,
554
555 #[clap(long)]
557 pub secure_boot: bool,
558
559 #[clap(long)]
561 pub secure_boot_template: Option<SecureBootTemplateCli>,
562
563 #[clap(long, value_name = "PATH")]
565 pub custom_uefi_json: Option<PathBuf>,
566
567 #[clap(long, hide(true))]
572 pub relay_console_path: Option<PathBuf>,
573
574 #[clap(long, hide(true))]
578 pub relay_console_title: Option<String>,
579
580 #[clap(long, value_name = "PORT")]
582 pub gdb: Option<u16>,
583
584 #[clap(long)]
589 pub mana: Vec<NicConfigCli>,
590
591 #[clap(long)]
605 pub hypervisor: Option<String>,
606
607 #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
615 pub custom_dsdt: Option<PathBuf>,
616
617 #[clap(long_help = r#"
627e.g: --ide memdiff:file:/path/to/disk.vhd
628
629syntax: <path> | kind:<arg>[,flag,opt=arg,...]
630
631valid disk kinds:
632 `mem:<len>` memory backed disk
633 <len>: length of ramdisk, e.g.: `1G`
634 `memdiff:<disk>` memory backed diff disk
635 <disk>: lower disk, e.g.: `file:base.img`
636 `file:<path>[;create=<len>]` file-backed disk
637 <path>: path to file
638 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
639 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
640 `blob:<type>:<url>` HTTP blob (read-only)
641 <type>: `flat` or `vhd1`
642 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
643 <cipher>: `xts-aes-256`
644
645additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
646this list is not exhaustive.
647
648flags:
649 `ro` open disk as read-only
650 `s` attach drive to secondary ide channel
651 `dvd` specifies that device is cd/dvd and it is read_only
652"#)]
653 #[clap(long, value_name = "FILE", requires("pcat"))]
654 pub ide: Vec<IdeDiskCli>,
655
656 #[clap(long_help = r#"
659e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
660
661syntax: <path> | kind:<arg>[,flag,opt=arg,...]
662
663valid disk kinds:
664 `mem:<len>` memory backed disk
665 <len>: length of ramdisk, e.g.: `1G`
666 `memdiff:<disk>` memory backed diff disk
667 <disk>: lower disk, e.g.: `file:base.img`
668 `file:<path>[;create=<len>]` file-backed disk
669 <path>: path to file
670 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
671 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
672 `blob:<type>:<url>` HTTP blob (read-only)
673 <type>: `flat` or `vhd1`
674 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
675 <cipher>: `xts-aes-256`
676
677flags:
678 `ro` open disk as read-only
679"#)]
680 #[clap(long, value_name = "FILE", requires("pcat"))]
681 pub floppy: Vec<FloppyDiskCli>,
682
683 #[clap(long)]
685 pub guest_watchdog: bool,
686
687 #[clap(long)]
689 pub openhcl_dump_path: Option<PathBuf>,
690
691 #[clap(long)]
693 pub halt_on_reset: bool,
694
695 #[clap(long)]
697 pub write_saved_state_proto: Option<PathBuf>,
698
699 #[clap(long)]
701 pub imc: Option<PathBuf>,
702
703 #[clap(long)]
705 pub battery: bool,
706
707 #[clap(long)]
709 pub uefi_console_mode: Option<UefiConsoleModeCli>,
710
711 #[clap(long_help = r#"
713Set the EFI diagnostics log level.
714
715options:
716 default default (ERROR and WARN only)
717 info info (ERROR, WARN, and INFO)
718 full full (all log levels)
719"#)]
720 #[clap(long, requires("uefi"))]
721 pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
722
723 #[clap(long)]
725 pub default_boot_always_attempt: bool,
726
727 #[clap(long_help = r#"
729Attach root complexes to the VM.
730
731Examples:
732 # Attach root complex rc0 on segment 0 with bus and MMIO ranges
733 --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
734
735Syntax: <name>[,opt=arg,...]
736
737Options:
738 `segment=<value>` configures the PCI Express segment, default 0
739 `start_bus=<value>` lowest valid bus number, default 0
740 `end_bus=<value>` highest valid bus number, default 255
741 `low_mmio=<size>` low MMIO window size, default 64M
742 `high_mmio=<size>` high MMIO window size, default 1G
743"#)]
744 #[clap(long, conflicts_with("pcat"))]
745 pub pcie_root_complex: Vec<PcieRootComplexCli>,
746
747 #[clap(long_help = r#"
749Attach root ports to root complexes.
750
751Examples:
752 # Attach root port rc0rp0 to root complex rc0
753 --pcie-root-port rc0:rc0rp0
754
755 # Attach root port rc0rp1 to root complex rc0 with hotplug support
756 --pcie-root-port rc0:rc0rp1,hotplug
757
758Syntax: <root_complex_name>:<name>[,hotplug]
759
760Options:
761 `hotplug` enable hotplug support for this root port
762"#)]
763 #[clap(long, conflicts_with("pcat"))]
764 pub pcie_root_port: Vec<PcieRootPortCli>,
765
766 #[clap(long_help = r#"
768Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
769
770Examples:
771 # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
772 --pcie-switch rp0:switch0,num_downstream_ports=4
773
774 # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
775 --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
776
777 # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
778 --pcie-switch rp0:switch0
779 --pcie-switch switch0-downstream-0:switch1
780 --pcie-switch switch1-downstream-1:switch2
781
782 # Enable hotplug on all downstream switch ports of switch0
783 --pcie-switch rp0:switch0,hotplug
784
785Syntax: <port_name>:<name>[,opt,opt=arg,...]
786
787 port_name can be:
788 - Root port name (e.g., "rp0") to connect directly to a root port
789 - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
790
791Options:
792 `hotplug` enable hotplug support for all downstream switch ports
793 `num_downstream_ports=<value>` number of downstream ports, default 4
794"#)]
795 #[clap(long, conflicts_with("pcat"))]
796 pub pcie_switch: Vec<GenericPcieSwitchCli>,
797
798 #[clap(long_help = r#"
800Attach PCIe devices to root ports or downstream switch ports
801which are implemented in a simulator running in a remote process.
802
803Examples:
804 # Attach to root port rc0rp0 with default socket
805 --pcie-remote rc0rp0
806
807 # Attach with custom socket address
808 --pcie-remote rc0rp0,socket=0.0.0.0:48914
809
810 # Specify HU and controller identifiers
811 --pcie-remote rc0rp0,hu=1,controller=0
812
813 # Multiple devices on different ports
814 --pcie-remote rc0rp0,socket=0.0.0.0:48914
815 --pcie-remote rc0rp1,socket=0.0.0.0:48915
816
817Syntax: <port_name>[,opt=arg,...]
818
819Options:
820 `socket=<address>` TCP socket (default: localhost:48914)
821 `hu=<value>` Hardware unit identifier (default: 0)
822 `controller=<value>` Controller identifier (default: 0)
823"#)]
824 #[clap(long, conflicts_with("pcat"))]
825 pub pcie_remote: Vec<PcieRemoteCli>,
826
827 #[clap(long_help = r#"
829Assign a host PCI device to the guest via Linux VFIO.
830
831The device must be bound to vfio-pci on the host before starting the VM.
832
833Examples:
834 # Assign NVMe controller to root port rp0
835 --vfio rp0:0000:01:00.0
836
837Syntax: <port_name>:<pci_bdf>
838
839 port_name Root port or downstream switch port name
840 pci_bdf PCI domain:bus:device.function of the VFIO device on
841 the host (use lspci -D to find it)
842"#)]
843 #[cfg(target_os = "linux")]
844 #[clap(long, conflicts_with("pcat"))]
845 pub vfio: Vec<VfioDeviceCli>,
846}
847
848#[derive(Clone, Debug, PartialEq)]
849pub struct FsArgs {
850 pub tag: String,
851 pub path: String,
852 pub pcie_port: Option<String>,
853}
854
855impl FromStr for FsArgs {
856 type Err = anyhow::Error;
857
858 fn from_str(s: &str) -> Result<Self, Self::Err> {
859 let (pcie_port, s) = parse_pcie_port_prefix(s);
860 let mut s = s.split(',');
861 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
862 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
863 };
864 Ok(Self {
865 tag: tag.to_owned(),
866 path: path.to_owned(),
867 pcie_port,
868 })
869 }
870}
871
872#[derive(Clone, Debug, PartialEq)]
873pub struct FsArgsWithOptions {
874 pub tag: String,
876 pub path: String,
878 pub options: String,
880 pub pcie_port: Option<String>,
882}
883
884impl FromStr for FsArgsWithOptions {
885 type Err = anyhow::Error;
886
887 fn from_str(s: &str) -> Result<Self, Self::Err> {
888 let (pcie_port, s) = parse_pcie_port_prefix(s);
889 let mut s = s.split(',');
890 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
891 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
892 };
893 let options = s.collect::<Vec<_>>().join(";");
894 Ok(Self {
895 tag: tag.to_owned(),
896 path: path.to_owned(),
897 options,
898 pcie_port,
899 })
900 }
901}
902
903#[derive(Copy, Clone, clap::ValueEnum)]
904pub enum VirtioBusCli {
905 Auto,
906 Mmio,
907 Pci,
908 Vpci,
909}
910
911fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
916 if let Some(rest) = s.strip_prefix("pcie_port=") {
917 if let Some((port, rest)) = rest.split_once(':') {
918 if !port.is_empty() {
919 return (Some(port.to_string()), rest);
920 }
921 }
922 }
923 (None, s)
924}
925
926#[derive(Clone, Debug, PartialEq)]
927pub struct VirtioPmemArgs {
928 pub path: String,
929 pub pcie_port: Option<String>,
930}
931
932impl FromStr for VirtioPmemArgs {
933 type Err = anyhow::Error;
934
935 fn from_str(s: &str) -> Result<Self, Self::Err> {
936 let (pcie_port, s) = parse_pcie_port_prefix(s);
937 if s.is_empty() {
938 anyhow::bail!("expected [pcie_port=<port>:]<path>");
939 }
940 Ok(Self {
941 path: s.to_owned(),
942 pcie_port,
943 })
944 }
945}
946
947#[derive(clap::ValueEnum, Clone, Copy)]
948pub enum SecureBootTemplateCli {
949 Windows,
950 UefiCa,
951}
952
953fn parse_memory(s: &str) -> anyhow::Result<u64> {
954 if s == "VMGS_DEFAULT" {
955 Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
956 } else {
957 || -> Option<u64> {
958 let mut b = s.as_bytes();
959 if s.ends_with('B') {
960 b = &b[..b.len() - 1]
961 }
962 if b.is_empty() {
963 return None;
964 }
965 let multi = match b[b.len() - 1] as char {
966 'T' => Some(1024 * 1024 * 1024 * 1024),
967 'G' => Some(1024 * 1024 * 1024),
968 'M' => Some(1024 * 1024),
969 'K' => Some(1024),
970 _ => None,
971 };
972 if multi.is_some() {
973 b = &b[..b.len() - 1]
974 }
975 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
976 Some(n * multi.unwrap_or(1))
977 }()
978 .with_context(|| format!("invalid memory size '{0}'", s))
979 }
980}
981
982fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
984 match s.strip_prefix("0x") {
985 Some(rest) => u64::from_str_radix(rest, 16),
986 None => s.parse::<u64>(),
987 }
988}
989
990#[derive(Clone, Debug, PartialEq)]
991pub enum DiskCliKind {
992 Memory(u64),
994 MemoryDiff(Box<DiskCliKind>),
996 Sqlite {
998 path: PathBuf,
999 create_with_len: Option<u64>,
1000 },
1001 SqliteDiff {
1003 path: PathBuf,
1004 create: bool,
1005 disk: Box<DiskCliKind>,
1006 },
1007 AutoCacheSqlite {
1009 cache_path: String,
1010 key: Option<String>,
1011 disk: Box<DiskCliKind>,
1012 },
1013 PersistentReservationsWrapper(Box<DiskCliKind>),
1015 File {
1017 path: PathBuf,
1018 create_with_len: Option<u64>,
1019 direct: bool,
1020 },
1021 Blob {
1023 kind: BlobKind,
1024 url: String,
1025 },
1026 Crypt {
1028 cipher: DiskCipher,
1029 key_file: PathBuf,
1030 disk: Box<DiskCliKind>,
1031 },
1032 DelayDiskWrapper {
1034 delay_ms: u64,
1035 disk: Box<DiskCliKind>,
1036 },
1037}
1038
1039#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
1040pub enum DiskCipher {
1041 #[clap(name = "xts-aes-256")]
1042 XtsAes256,
1043}
1044
1045#[derive(Copy, Clone, Debug, PartialEq)]
1046pub enum BlobKind {
1047 Flat,
1048 Vhd1,
1049}
1050
1051struct FileOpts {
1052 path: PathBuf,
1053 create_with_len: Option<u64>,
1054 direct: bool,
1055}
1056
1057fn parse_file_opts(arg: &str) -> anyhow::Result<FileOpts> {
1058 let mut path = arg;
1059 let mut create_with_len = None;
1060 let mut direct = false;
1061
1062 if let Some((p, rest)) = arg.split_once(';') {
1064 path = p;
1065 for opt in rest.split(';') {
1066 if let Some(len) = opt.strip_prefix("create=") {
1067 create_with_len = Some(parse_memory(len)?);
1068 } else if opt == "direct" {
1069 direct = true;
1070 } else {
1071 anyhow::bail!("invalid file option '{opt}', expected 'create=<len>' or 'direct'");
1072 }
1073 }
1074 }
1075
1076 Ok(FileOpts {
1077 path: path.into(),
1078 create_with_len,
1079 direct,
1080 })
1081}
1082
1083impl DiskCliKind {
1084 fn parse_autocache(
1087 arg: &str,
1088 cache_path: Result<String, std::env::VarError>,
1089 ) -> anyhow::Result<Self> {
1090 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
1091 let cache_path = cache_path.context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
1092 Ok(DiskCliKind::AutoCacheSqlite {
1093 cache_path,
1094 key: (!key.is_empty()).then(|| key.to_string()),
1095 disk: Box::new(kind.parse()?),
1096 })
1097 }
1098}
1099
1100impl FromStr for DiskCliKind {
1101 type Err = anyhow::Error;
1102
1103 fn from_str(s: &str) -> anyhow::Result<Self> {
1104 let disk = match s.split_once(':') {
1105 None => {
1107 let FileOpts {
1108 path,
1109 create_with_len,
1110 direct,
1111 } = parse_file_opts(s)?;
1112 DiskCliKind::File {
1113 path,
1114 create_with_len,
1115 direct,
1116 }
1117 }
1118 Some((kind, arg)) => match kind {
1119 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
1120 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
1121 "sql" => {
1122 let FileOpts {
1123 path,
1124 create_with_len,
1125 direct,
1126 } = parse_file_opts(arg)?;
1127 if direct {
1128 anyhow::bail!("'direct' is not supported for 'sql' disks");
1129 }
1130 DiskCliKind::Sqlite {
1131 path,
1132 create_with_len,
1133 }
1134 }
1135 "sqldiff" => {
1136 let (path_and_opts, kind) =
1137 arg.split_once(':').context("expected path[;opts]:kind")?;
1138 let disk = Box::new(kind.parse()?);
1139 match path_and_opts.split_once(';') {
1140 Some((path, create)) => {
1141 if create != "create" {
1142 anyhow::bail!("invalid syntax after ';', expected 'create'")
1143 }
1144 DiskCliKind::SqliteDiff {
1145 path: path.into(),
1146 create: true,
1147 disk,
1148 }
1149 }
1150 None => DiskCliKind::SqliteDiff {
1151 path: path_and_opts.into(),
1152 create: false,
1153 disk,
1154 },
1155 }
1156 }
1157 "autocache" => {
1158 Self::parse_autocache(arg, std::env::var("OPENVMM_AUTO_CACHE_PATH"))?
1159 }
1160 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
1161 "file" => {
1162 let FileOpts {
1163 path,
1164 create_with_len,
1165 direct,
1166 } = parse_file_opts(arg)?;
1167 DiskCliKind::File {
1168 path,
1169 create_with_len,
1170 direct,
1171 }
1172 }
1173 "blob" => {
1174 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
1175 let blob_kind = match blob_kind {
1176 "flat" => BlobKind::Flat,
1177 "vhd1" => BlobKind::Vhd1,
1178 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
1179 };
1180 DiskCliKind::Blob {
1181 kind: blob_kind,
1182 url: url.to_string(),
1183 }
1184 }
1185 "crypt" => {
1186 let (cipher, (key, kind)) = arg
1187 .split_once(':')
1188 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
1189 .context("expected cipher:key_file:kind")?;
1190 DiskCliKind::Crypt {
1191 cipher: ValueEnum::from_str(cipher, false)
1192 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
1193 key_file: PathBuf::from(key),
1194 disk: Box::new(kind.parse()?),
1195 }
1196 }
1197 kind => {
1198 let FileOpts {
1203 path,
1204 create_with_len,
1205 direct,
1206 } = parse_file_opts(s)?;
1207 if path.has_root() {
1208 DiskCliKind::File {
1209 path,
1210 create_with_len,
1211 direct,
1212 }
1213 } else {
1214 anyhow::bail!("invalid disk kind {kind}");
1215 }
1216 }
1217 },
1218 };
1219 Ok(disk)
1220 }
1221}
1222
1223#[derive(Clone)]
1224pub struct VmgsCli {
1225 pub kind: DiskCliKind,
1226 pub provision: ProvisionVmgs,
1227}
1228
1229#[derive(Copy, Clone)]
1230pub enum ProvisionVmgs {
1231 OnEmpty,
1232 OnFailure,
1233 True,
1234}
1235
1236impl FromStr for VmgsCli {
1237 type Err = anyhow::Error;
1238
1239 fn from_str(s: &str) -> anyhow::Result<Self> {
1240 let (kind, opt) = s
1241 .split_once(',')
1242 .map(|(k, o)| (k, Some(o)))
1243 .unwrap_or((s, None));
1244 let kind = kind.parse()?;
1245
1246 let provision = match opt {
1247 None => ProvisionVmgs::OnEmpty,
1248 Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
1249 Some("fmt") => ProvisionVmgs::True,
1250 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
1251 };
1252
1253 Ok(VmgsCli { kind, provision })
1254 }
1255}
1256
1257#[derive(Clone)]
1259pub struct DiskCli {
1260 pub vtl: DeviceVtl,
1261 pub kind: DiskCliKind,
1262 pub read_only: bool,
1263 pub is_dvd: bool,
1264 pub underhill: Option<UnderhillDiskSource>,
1265 pub pcie_port: Option<String>,
1266}
1267
1268#[derive(Copy, Clone)]
1269pub enum UnderhillDiskSource {
1270 Scsi,
1271 Nvme,
1272}
1273
1274impl FromStr for DiskCli {
1275 type Err = anyhow::Error;
1276
1277 fn from_str(s: &str) -> anyhow::Result<Self> {
1278 let mut opts = s.split(',');
1279 let kind = opts.next().unwrap().parse()?;
1280
1281 let mut read_only = false;
1282 let mut is_dvd = false;
1283 let mut underhill = None;
1284 let mut vtl = DeviceVtl::Vtl0;
1285 let mut pcie_port = None;
1286 for opt in opts {
1287 let mut s = opt.split('=');
1288 let opt = s.next().unwrap();
1289 match opt {
1290 "ro" => read_only = true,
1291 "dvd" => {
1292 is_dvd = true;
1293 read_only = true;
1294 }
1295 "vtl2" => {
1296 vtl = DeviceVtl::Vtl2;
1297 }
1298 "uh" => underhill = Some(UnderhillDiskSource::Scsi),
1299 "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
1300 "pcie_port" => {
1301 let port = s.next();
1302 if port.is_none_or(|p| p.is_empty()) {
1303 anyhow::bail!("`pcie_port` requires a port name");
1304 }
1305 pcie_port = Some(String::from(port.unwrap()));
1306 }
1307 opt => anyhow::bail!("unknown option: '{opt}'"),
1308 }
1309 }
1310
1311 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
1312 anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
1313 }
1314
1315 if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
1316 anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
1317 }
1318
1319 Ok(DiskCli {
1320 vtl,
1321 kind,
1322 read_only,
1323 is_dvd,
1324 underhill,
1325 pcie_port,
1326 })
1327 }
1328}
1329
1330#[derive(Clone)]
1332pub struct IdeDiskCli {
1333 pub kind: DiskCliKind,
1334 pub read_only: bool,
1335 pub channel: Option<u8>,
1336 pub device: Option<u8>,
1337 pub is_dvd: bool,
1338}
1339
1340impl FromStr for IdeDiskCli {
1341 type Err = anyhow::Error;
1342
1343 fn from_str(s: &str) -> anyhow::Result<Self> {
1344 let mut opts = s.split(',');
1345 let kind = opts.next().unwrap().parse()?;
1346
1347 let mut read_only = false;
1348 let mut channel = None;
1349 let mut device = None;
1350 let mut is_dvd = false;
1351 for opt in opts {
1352 let mut s = opt.split('=');
1353 let opt = s.next().unwrap();
1354 match opt {
1355 "ro" => read_only = true,
1356 "p" => channel = Some(0),
1357 "s" => channel = Some(1),
1358 "0" => device = Some(0),
1359 "1" => device = Some(1),
1360 "dvd" => {
1361 is_dvd = true;
1362 read_only = true;
1363 }
1364 _ => anyhow::bail!("unknown option: '{opt}'"),
1365 }
1366 }
1367
1368 Ok(IdeDiskCli {
1369 kind,
1370 read_only,
1371 channel,
1372 device,
1373 is_dvd,
1374 })
1375 }
1376}
1377
1378#[derive(Clone, Debug, PartialEq)]
1380pub struct FloppyDiskCli {
1381 pub kind: DiskCliKind,
1382 pub read_only: bool,
1383}
1384
1385impl FromStr for FloppyDiskCli {
1386 type Err = anyhow::Error;
1387
1388 fn from_str(s: &str) -> anyhow::Result<Self> {
1389 if s.is_empty() {
1390 anyhow::bail!("empty disk spec");
1391 }
1392 let mut opts = s.split(',');
1393 let kind = opts.next().unwrap().parse()?;
1394
1395 let mut read_only = false;
1396 for opt in opts {
1397 let mut s = opt.split('=');
1398 let opt = s.next().unwrap();
1399 match opt {
1400 "ro" => read_only = true,
1401 _ => anyhow::bail!("unknown option: '{opt}'"),
1402 }
1403 }
1404
1405 Ok(FloppyDiskCli { kind, read_only })
1406 }
1407}
1408
1409#[derive(Clone)]
1410pub struct DebugconSerialConfigCli {
1411 pub port: u16,
1412 pub serial: SerialConfigCli,
1413}
1414
1415impl FromStr for DebugconSerialConfigCli {
1416 type Err = String;
1417
1418 fn from_str(s: &str) -> Result<Self, Self::Err> {
1419 let Some((port, serial)) = s.split_once(',') else {
1420 return Err("invalid format (missing comma between port and serial)".into());
1421 };
1422
1423 let port: u16 = parse_number(port)
1424 .map_err(|_| "could not parse port".to_owned())?
1425 .try_into()
1426 .map_err(|_| "port must be 16-bit")?;
1427 let serial: SerialConfigCli = serial.parse()?;
1428
1429 Ok(Self { port, serial })
1430 }
1431}
1432
1433#[derive(Clone, Debug, PartialEq)]
1435pub enum SerialConfigCli {
1436 None,
1437 Console,
1438 NewConsole(Option<PathBuf>, Option<String>),
1439 Stderr,
1440 Pipe(PathBuf),
1441 Tcp(SocketAddr),
1442 File(PathBuf),
1443}
1444
1445impl FromStr for SerialConfigCli {
1446 type Err = String;
1447
1448 fn from_str(s: &str) -> Result<Self, Self::Err> {
1449 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1450
1451 let first_key = match keyvalues.first() {
1452 Some(first_pair) => first_pair.0.as_str(),
1453 None => Err("invalid serial configuration: no values supplied")?,
1454 };
1455 let first_value = keyvalues.first().unwrap().1.as_ref();
1456
1457 let ret = match first_key {
1458 "none" => SerialConfigCli::None,
1459 "console" => SerialConfigCli::Console,
1460 "stderr" => SerialConfigCli::Stderr,
1461 "file" => match first_value {
1462 Some(path) => SerialConfigCli::File(path.into()),
1463 None => Err("invalid serial configuration: file requires a value")?,
1464 },
1465 "term" => {
1466 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1468 let window_name = match window_name {
1469 Some((_, Some(name))) => Some(name.clone()),
1470 _ => None,
1471 };
1472
1473 SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
1474 }
1475 "listen" => match first_value {
1476 Some(path) => {
1477 if let Some(tcp) = path.strip_prefix("tcp:") {
1478 let addr = tcp
1479 .parse()
1480 .map_err(|err| format!("invalid tcp address: {err}"))?;
1481 SerialConfigCli::Tcp(addr)
1482 } else {
1483 SerialConfigCli::Pipe(path.into())
1484 }
1485 }
1486 None => Err(
1487 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1488 )?,
1489 },
1490 _ => {
1491 return Err(format!(
1492 "invalid serial configuration: '{}' is not a known option",
1493 first_key
1494 ));
1495 }
1496 };
1497
1498 Ok(ret)
1499 }
1500}
1501
1502impl SerialConfigCli {
1503 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1506 let mut ret = Vec::new();
1507
1508 for item in s.split(',') {
1510 let mut eqsplit = item.split('=');
1513 let key = eqsplit.next();
1514 let value = eqsplit.next();
1515
1516 if let Some(key) = key {
1517 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1518 } else {
1519 return Err("invalid key=value pair in serial config".into());
1521 }
1522 }
1523 Ok(ret)
1524 }
1525}
1526
1527#[derive(Clone, Debug, PartialEq)]
1528pub enum EndpointConfigCli {
1529 None,
1530 Consomme { cidr: Option<String> },
1531 Dio { id: Option<String> },
1532 Tap { name: String },
1533}
1534
1535impl FromStr for EndpointConfigCli {
1536 type Err = String;
1537
1538 fn from_str(s: &str) -> Result<Self, Self::Err> {
1539 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1540 ["none"] => EndpointConfigCli::None,
1541 ["consomme", s @ ..] => EndpointConfigCli::Consomme {
1542 cidr: s.first().map(|&s| s.to_owned()),
1543 },
1544 ["dio", s @ ..] => EndpointConfigCli::Dio {
1545 id: s.first().map(|s| (*s).to_owned()),
1546 },
1547 ["tap", name] => EndpointConfigCli::Tap {
1548 name: (*name).to_owned(),
1549 },
1550 _ => return Err("invalid network backend".into()),
1551 };
1552
1553 Ok(ret)
1554 }
1555}
1556
1557#[derive(Clone, Debug, PartialEq)]
1558pub struct NicConfigCli {
1559 pub vtl: DeviceVtl,
1560 pub endpoint: EndpointConfigCli,
1561 pub max_queues: Option<u16>,
1562 pub underhill: bool,
1563 pub pcie_port: Option<String>,
1564}
1565
1566impl FromStr for NicConfigCli {
1567 type Err = String;
1568
1569 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1570 let mut vtl = DeviceVtl::Vtl0;
1571 let mut max_queues = None;
1572 let mut underhill = false;
1573 let mut pcie_port = None;
1574 while let Some((opt, rest)) = s.split_once(':') {
1575 if let Some((opt, val)) = opt.split_once('=') {
1576 match opt {
1577 "queues" => {
1578 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1579 }
1580 "pcie_port" => {
1581 if val.is_empty() {
1582 return Err("`pcie_port=` requires port name argument".into());
1583 }
1584 pcie_port = Some(val.to_string());
1585 }
1586 _ => break,
1587 }
1588 } else {
1589 match opt {
1590 "vtl2" => {
1591 vtl = DeviceVtl::Vtl2;
1592 }
1593 "uh" => underhill = true,
1594 _ => break,
1595 }
1596 }
1597 s = rest;
1598 }
1599
1600 if underhill && vtl != DeviceVtl::Vtl0 {
1601 return Err("`uh` is incompatible with `vtl2`".into());
1602 }
1603
1604 if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
1605 return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
1606 }
1607
1608 let endpoint = s.parse()?;
1609 Ok(NicConfigCli {
1610 vtl,
1611 endpoint,
1612 max_queues,
1613 underhill,
1614 pcie_port,
1615 })
1616 }
1617}
1618
1619#[derive(Debug, Error)]
1620#[error("unknown VTL2 relocation type: {0}")]
1621pub struct UnknownVtl2RelocationType(String);
1622
1623fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1624 match s {
1625 "disable" => Ok(Vtl2BaseAddressType::File),
1626 s if s.starts_with("auto=") => {
1627 let s = s.strip_prefix("auto=").unwrap_or_default();
1628 let size = if s == "filesize" {
1629 None
1630 } else {
1631 let size = parse_memory(s).map_err(|e| {
1632 UnknownVtl2RelocationType(format!(
1633 "unable to parse memory size from {} for 'auto=' type, {e}",
1634 e
1635 ))
1636 })?;
1637 Some(size)
1638 };
1639 Ok(Vtl2BaseAddressType::MemoryLayout { size })
1640 }
1641 s if s.starts_with("absolute=") => {
1642 let s = s.strip_prefix("absolute=");
1643 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
1644 UnknownVtl2RelocationType(format!(
1645 "unable to parse number from {} for 'absolute=' type",
1646 e
1647 ))
1648 })?;
1649 Ok(Vtl2BaseAddressType::Absolute(addr))
1650 }
1651 s if s.starts_with("vtl2=") => {
1652 let s = s.strip_prefix("vtl2=").unwrap_or_default();
1653 let size = if s == "filesize" {
1654 None
1655 } else {
1656 let size = parse_memory(s).map_err(|e| {
1657 UnknownVtl2RelocationType(format!(
1658 "unable to parse memory size from {} for 'vtl2=' type, {e}",
1659 e
1660 ))
1661 })?;
1662 Some(size)
1663 };
1664 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
1665 }
1666 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
1667 }
1668}
1669
1670#[derive(Debug, Copy, Clone, PartialEq)]
1671pub enum SmtConfigCli {
1672 Auto,
1673 Force,
1674 Off,
1675}
1676
1677#[derive(Debug, Error)]
1678#[error("expected auto, force, or off")]
1679pub struct BadSmtConfig;
1680
1681impl FromStr for SmtConfigCli {
1682 type Err = BadSmtConfig;
1683
1684 fn from_str(s: &str) -> Result<Self, Self::Err> {
1685 let r = match s {
1686 "auto" => Self::Auto,
1687 "force" => Self::Force,
1688 "off" => Self::Off,
1689 _ => return Err(BadSmtConfig),
1690 };
1691 Ok(r)
1692 }
1693}
1694
1695#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
1696fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
1697 let r = match s {
1698 "auto" => X2ApicConfig::Auto,
1699 "supported" => X2ApicConfig::Supported,
1700 "off" => X2ApicConfig::Unsupported,
1701 "on" => X2ApicConfig::Enabled,
1702 _ => return Err("expected auto, supported, off, or on"),
1703 };
1704 Ok(r)
1705}
1706
1707#[derive(Debug, Copy, Clone, ValueEnum)]
1708pub enum Vtl0LateMapPolicyCli {
1709 Off,
1710 Log,
1711 Halt,
1712 Exception,
1713}
1714
1715#[derive(Debug, Copy, Clone, ValueEnum)]
1716pub enum IsolationCli {
1717 Vbs,
1718}
1719
1720#[derive(Debug, Copy, Clone, PartialEq)]
1721pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
1722
1723impl FromStr for PcatBootOrderCli {
1724 type Err = &'static str;
1725
1726 fn from_str(s: &str) -> Result<Self, Self::Err> {
1727 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
1728 let mut order = Vec::new();
1729
1730 for item in s.split(',') {
1731 let device = match item {
1732 "optical" => PcatBootDevice::Optical,
1733 "hdd" => PcatBootDevice::HardDrive,
1734 "net" => PcatBootDevice::Network,
1735 "floppy" => PcatBootDevice::Floppy,
1736 _ => return Err("unknown boot device type"),
1737 };
1738
1739 let default_pos = default_order
1740 .iter()
1741 .position(|x| x == &Some(device))
1742 .ok_or("cannot pass duplicate boot devices")?;
1743
1744 order.push(default_order[default_pos].take().unwrap());
1745 }
1746
1747 order.extend(default_order.into_iter().flatten());
1748 assert_eq!(order.len(), 4);
1749
1750 Ok(Self(order.try_into().unwrap()))
1751 }
1752}
1753
1754#[derive(Copy, Clone, Debug, ValueEnum)]
1755pub enum UefiConsoleModeCli {
1756 Default,
1757 Com1,
1758 Com2,
1759 None,
1760}
1761
1762#[derive(Copy, Clone, Debug, Default, ValueEnum)]
1763pub enum EfiDiagnosticsLogLevelCli {
1764 #[default]
1765 Default,
1766 Info,
1767 Full,
1768}
1769
1770#[derive(Clone, Debug, PartialEq)]
1771pub struct PcieRootComplexCli {
1772 pub name: String,
1773 pub segment: u16,
1774 pub start_bus: u8,
1775 pub end_bus: u8,
1776 pub low_mmio: u32,
1777 pub high_mmio: u64,
1778}
1779
1780impl FromStr for PcieRootComplexCli {
1781 type Err = anyhow::Error;
1782
1783 fn from_str(s: &str) -> Result<Self, Self::Err> {
1784 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(',');
1788 let name = opts.next().context("expected root complex name")?;
1789 if name.is_empty() {
1790 anyhow::bail!("must provide a root complex name");
1791 }
1792
1793 let mut segment = 0;
1794 let mut start_bus = 0;
1795 let mut end_bus = 255;
1796 let mut low_mmio = DEFAULT_PCIE_CRS_LOW_SIZE;
1797 let mut high_mmio = DEFAULT_PCIE_CRS_HIGH_SIZE;
1798 for opt in opts {
1799 let mut s = opt.split('=');
1800 let opt = s.next().context("expected option")?;
1801 match opt {
1802 "segment" => {
1803 let seg_str = s.next().context("expected segment number")?;
1804 segment = u16::from_str(seg_str).context("failed to parse segment number")?;
1805 }
1806 "start_bus" => {
1807 let bus_str = s.next().context("expected start bus number")?;
1808 start_bus =
1809 u8::from_str(bus_str).context("failed to parse start bus number")?;
1810 }
1811 "end_bus" => {
1812 let bus_str = s.next().context("expected end bus number")?;
1813 end_bus = u8::from_str(bus_str).context("failed to parse end bus number")?;
1814 }
1815 "low_mmio" => {
1816 let low_mmio_str = s.next().context("expected low MMIO size")?;
1817 low_mmio = parse_memory(low_mmio_str)
1818 .context("failed to parse low MMIO size")?
1819 .try_into()?;
1820 }
1821 "high_mmio" => {
1822 let high_mmio_str = s.next().context("expected high MMIO size")?;
1823 high_mmio =
1824 parse_memory(high_mmio_str).context("failed to parse high MMIO size")?;
1825 }
1826 opt => anyhow::bail!("unknown option: '{opt}'"),
1827 }
1828 }
1829
1830 if start_bus >= end_bus {
1831 anyhow::bail!("start_bus must be less than or equal to end_bus");
1832 }
1833
1834 Ok(PcieRootComplexCli {
1835 name: name.to_string(),
1836 segment,
1837 start_bus,
1838 end_bus,
1839 low_mmio,
1840 high_mmio,
1841 })
1842 }
1843}
1844
1845#[derive(Clone, Debug, PartialEq)]
1846pub struct PcieRootPortCli {
1847 pub root_complex_name: String,
1848 pub name: String,
1849 pub hotplug: bool,
1850}
1851
1852impl FromStr for PcieRootPortCli {
1853 type Err = anyhow::Error;
1854
1855 fn from_str(s: &str) -> Result<Self, Self::Err> {
1856 let mut opts = s.split(',');
1857 let names = opts.next().context("expected root port identifiers")?;
1858 if names.is_empty() {
1859 anyhow::bail!("must provide root port identifiers");
1860 }
1861
1862 let mut s = names.split(':');
1863 let rc_name = s.next().context("expected name of parent root complex")?;
1864 let rp_name = s.next().context("expected root port name")?;
1865
1866 if let Some(extra) = s.next() {
1867 anyhow::bail!("unexpected token: '{extra}'")
1868 }
1869
1870 let mut hotplug = false;
1871
1872 for opt in opts {
1874 match opt {
1875 "hotplug" => hotplug = true,
1876 _ => anyhow::bail!("unexpected option: '{opt}'"),
1877 }
1878 }
1879
1880 Ok(PcieRootPortCli {
1881 root_complex_name: rc_name.to_string(),
1882 name: rp_name.to_string(),
1883 hotplug,
1884 })
1885 }
1886}
1887
1888#[derive(Clone, Debug, PartialEq)]
1889pub struct GenericPcieSwitchCli {
1890 pub port_name: String,
1891 pub name: String,
1892 pub num_downstream_ports: u8,
1893 pub hotplug: bool,
1894}
1895
1896impl FromStr for GenericPcieSwitchCli {
1897 type Err = anyhow::Error;
1898
1899 fn from_str(s: &str) -> Result<Self, Self::Err> {
1900 let mut opts = s.split(',');
1901 let names = opts.next().context("expected switch identifiers")?;
1902 if names.is_empty() {
1903 anyhow::bail!("must provide switch identifiers");
1904 }
1905
1906 let mut s = names.split(':');
1907 let port_name = s.next().context("expected name of parent port")?;
1908 let switch_name = s.next().context("expected switch name")?;
1909
1910 if let Some(extra) = s.next() {
1911 anyhow::bail!("unexpected token: '{extra}'")
1912 }
1913
1914 let mut num_downstream_ports = 4u8; let mut hotplug = false;
1916
1917 for opt in opts {
1918 let mut kv = opt.split('=');
1919 let key = kv.next().context("expected option name")?;
1920
1921 match key {
1922 "num_downstream_ports" => {
1923 let value = kv.next().context("expected option value")?;
1924 if let Some(extra) = kv.next() {
1925 anyhow::bail!("unexpected token: '{extra}'")
1926 }
1927 num_downstream_ports = value.parse().context("invalid num_downstream_ports")?;
1928 }
1929 "hotplug" => {
1930 if kv.next().is_some() {
1931 anyhow::bail!("hotplug option does not take a value")
1932 }
1933 hotplug = true;
1934 }
1935 _ => anyhow::bail!("unknown option: '{key}'"),
1936 }
1937 }
1938
1939 Ok(GenericPcieSwitchCli {
1940 port_name: port_name.to_string(),
1941 name: switch_name.to_string(),
1942 num_downstream_ports,
1943 hotplug,
1944 })
1945 }
1946}
1947
1948#[derive(Clone, Debug, PartialEq)]
1950pub struct PcieRemoteCli {
1951 pub port_name: String,
1953 pub socket_addr: Option<String>,
1955 pub hu: u16,
1957 pub controller: u16,
1959}
1960
1961impl FromStr for PcieRemoteCli {
1962 type Err = anyhow::Error;
1963
1964 fn from_str(s: &str) -> Result<Self, Self::Err> {
1965 let mut opts = s.split(',');
1966 let port_name = opts.next().context("expected port name")?;
1967 if port_name.is_empty() {
1968 anyhow::bail!("must provide a port name");
1969 }
1970
1971 let mut socket_addr = None;
1972 let mut hu = 0u16;
1973 let mut controller = 0u16;
1974
1975 for opt in opts {
1976 let mut kv = opt.split('=');
1977 let key = kv.next().context("expected option name")?;
1978 let value = kv.next();
1979
1980 match key {
1981 "socket" => {
1982 let addr = value.context("socket requires an address")?;
1983 if let Some(extra) = kv.next() {
1984 anyhow::bail!("unexpected token: '{extra}'")
1985 }
1986 if addr.is_empty() {
1987 anyhow::bail!("socket address cannot be empty");
1988 }
1989 socket_addr = Some(addr.to_string());
1990 }
1991 "hu" => {
1992 let val = value.context("hu requires a value")?;
1993 if let Some(extra) = kv.next() {
1994 anyhow::bail!("unexpected token: '{extra}'")
1995 }
1996 hu = val.parse().context("failed to parse hu")?;
1997 }
1998 "controller" => {
1999 let val = value.context("controller requires a value")?;
2000 if let Some(extra) = kv.next() {
2001 anyhow::bail!("unexpected token: '{extra}'")
2002 }
2003 controller = val.parse().context("failed to parse controller")?;
2004 }
2005 _ => anyhow::bail!("unknown option: '{key}'"),
2006 }
2007 }
2008
2009 Ok(PcieRemoteCli {
2010 port_name: port_name.to_string(),
2011 socket_addr,
2012 hu,
2013 controller,
2014 })
2015 }
2016}
2017
2018#[cfg(target_os = "linux")]
2020#[derive(Clone, Debug)]
2021pub struct VfioDeviceCli {
2022 pub port_name: String,
2024 pub pci_id: String,
2026}
2027
2028#[cfg(target_os = "linux")]
2029impl FromStr for VfioDeviceCli {
2030 type Err = anyhow::Error;
2031
2032 fn from_str(s: &str) -> Result<Self, Self::Err> {
2033 let (port_name, pci_id) = s
2034 .split_once(':')
2035 .context("expected <port_name>:<pci_bdf> (e.g., rp0:0000:01:00.0)")?;
2036
2037 if port_name.is_empty() {
2038 anyhow::bail!("port name cannot be empty");
2039 }
2040
2041 if pci_id.is_empty() {
2042 anyhow::bail!("PCI address cannot be empty");
2043 }
2044
2045 if pci_id.contains('/') || pci_id.contains("..") {
2047 anyhow::bail!("PCI address must not contain path separators");
2048 }
2049
2050 Ok(VfioDeviceCli {
2051 port_name: port_name.to_string(),
2052 pci_id: pci_id.to_string(),
2053 })
2054 }
2055}
2056
2057fn default_value_from_arch_env(name: &str) -> OsString {
2065 let prefix = if cfg!(guest_arch = "x86_64") {
2066 "X86_64"
2067 } else if cfg!(guest_arch = "aarch64") {
2068 "AARCH64"
2069 } else {
2070 return Default::default();
2071 };
2072 let prefixed = format!("{}_{}", prefix, name);
2073 std::env::var_os(name)
2074 .or_else(|| std::env::var_os(prefixed))
2075 .unwrap_or_default()
2076}
2077
2078#[derive(Clone)]
2080pub struct OptionalPathBuf(pub Option<PathBuf>);
2081
2082impl From<&std::ffi::OsStr> for OptionalPathBuf {
2083 fn from(s: &std::ffi::OsStr) -> Self {
2084 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
2085 }
2086}
2087
2088#[cfg(target_os = "linux")]
2089#[derive(Clone)]
2090pub enum VhostUserDeviceTypeCli {
2091 Blk {
2094 num_queues: Option<u16>,
2095 queue_size: Option<u16>,
2096 },
2097 Fs {
2099 tag: String,
2100 num_queues: Option<u16>,
2101 queue_size: Option<u16>,
2102 },
2103 Other {
2105 device_id: u16,
2106 queue_sizes: Vec<u16>,
2107 },
2108}
2109
2110#[cfg(target_os = "linux")]
2111#[derive(Clone)]
2112pub struct VhostUserCli {
2113 pub socket_path: String,
2114 pub device_type: VhostUserDeviceTypeCli,
2115 pub pcie_port: Option<String>,
2116}
2117
2118#[cfg(target_os = "linux")]
2122fn split_respecting_brackets(s: &str) -> anyhow::Result<Vec<&str>> {
2123 let mut result = Vec::new();
2124 let mut start = 0;
2125 let mut depth: i32 = 0;
2126 for (i, c) in s.char_indices() {
2127 match c {
2128 '[' => depth += 1,
2129 ']' => {
2130 depth -= 1;
2131 anyhow::ensure!(depth >= 0, "unmatched ']' in option string");
2132 }
2133 ',' if depth == 0 => {
2134 result.push(&s[start..i]);
2135 start = i + 1;
2136 }
2137 _ => {}
2138 }
2139 }
2140 anyhow::ensure!(depth == 0, "unclosed '[' in option string");
2141 result.push(&s[start..]);
2142 Ok(result)
2143}
2144
2145#[cfg(target_os = "linux")]
2146impl FromStr for VhostUserCli {
2147 type Err = anyhow::Error;
2148
2149 fn from_str(s: &str) -> anyhow::Result<Self> {
2150 let parts = split_respecting_brackets(s)?;
2152 let mut parts_iter = parts.into_iter();
2153 let socket_path = parts_iter
2154 .next()
2155 .context("missing socket path")?
2156 .to_string();
2157
2158 let mut device_id: Option<u16> = None;
2159 let mut tag: Option<String> = None;
2160 let mut pcie_port: Option<String> = None;
2161 let mut type_name = None;
2162 let mut num_queues: Option<u16> = None;
2163 let mut queue_size: Option<u16> = None;
2164 let mut queue_sizes: Option<Vec<u16>> = None;
2165 for opt in parts_iter {
2166 let (key, val) = opt.split_once('=').context("expected key=value option")?;
2167 match key {
2168 "type" => {
2169 type_name = Some(val);
2170 }
2171 "device_id" => {
2172 device_id = Some(val.parse().context("invalid device_id")?);
2173 }
2174 "tag" => {
2175 tag = Some(val.to_string());
2176 }
2177 "pcie_port" => {
2178 pcie_port = Some(val.to_string());
2179 }
2180 "num_queues" => {
2181 num_queues = Some(val.parse().context("invalid num_queues")?);
2182 }
2183 "queue_size" => {
2184 queue_size = Some(val.parse().context("invalid queue_size")?);
2185 }
2186 "queue_sizes" => {
2187 let trimmed = val
2189 .strip_prefix('[')
2190 .and_then(|v| v.strip_suffix(']'))
2191 .context("queue_sizes must be bracketed: [N,N,N]")?;
2192 let sizes: Vec<u16> = trimmed
2193 .split(',')
2194 .map(|s| s.parse().context("invalid queue size in queue_sizes"))
2195 .collect::<anyhow::Result<_>>()?;
2196 anyhow::ensure!(!sizes.is_empty(), "queue_sizes must be non-empty");
2197 queue_sizes = Some(sizes);
2198 }
2199 other => anyhow::bail!("unknown vhost-user option: '{other}'"),
2200 }
2201 }
2202
2203 if type_name.is_some() == device_id.is_some() {
2204 anyhow::bail!("must specify type=<name> or device_id=<N>");
2205 }
2206
2207 let device_type = match type_name {
2209 Some("fs") => {
2210 let tag = tag.take().context("type=fs requires tag=<name>")?;
2211 VhostUserDeviceTypeCli::Fs {
2212 tag,
2213 num_queues: num_queues.take(),
2214 queue_size: queue_size.take(),
2215 }
2216 }
2217 Some("blk") => VhostUserDeviceTypeCli::Blk {
2218 num_queues: num_queues.take(),
2219 queue_size: queue_size.take(),
2220 },
2221 Some(ty) => anyhow::bail!("unknown vhost-user device type: '{ty}'"),
2222 None => {
2223 let queue_sizes = queue_sizes
2224 .take()
2225 .context("device_id= requires queue_sizes=[N,N,...]")?;
2226 VhostUserDeviceTypeCli::Other {
2227 device_id: device_id.unwrap(),
2228 queue_sizes,
2229 }
2230 }
2231 };
2232
2233 if tag.is_some() {
2234 anyhow::bail!("tag= is only valid for type=fs");
2235 }
2236 if queue_sizes.is_some() {
2237 anyhow::bail!("queue_sizes= is only valid for device_id=");
2238 }
2239 if num_queues.is_some() || queue_size.is_some() {
2240 anyhow::bail!(
2241 "num_queues= and queue_size= are not valid for device_id=; use queue_sizes="
2242 );
2243 }
2244
2245 Ok(VhostUserCli {
2246 socket_path,
2247 device_type,
2248 pcie_port,
2249 })
2250 }
2251}
2252
2253#[cfg(test)]
2254mod tests {
2255 use super::*;
2256
2257 use std::path::Path;
2258
2259 #[test]
2260 fn test_parse_file_opts() {
2261 let disk = DiskCliKind::from_str("file:test.vhd;create=1G").unwrap();
2263 assert!(matches!(
2264 &disk,
2265 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
2266 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
2267 ));
2268
2269 let disk = DiskCliKind::from_str("test.vhd;create=1G").unwrap();
2271 assert!(matches!(
2272 &disk,
2273 DiskCliKind::File { path, create_with_len: Some(len), direct: false }
2274 if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
2275 ));
2276
2277 let disk = DiskCliKind::from_str("file:/dev/sdb;direct").unwrap();
2279 assert!(matches!(
2280 &disk,
2281 DiskCliKind::File { path, create_with_len: None, direct: true }
2282 if path == Path::new("/dev/sdb")
2283 ));
2284
2285 let disk = DiskCliKind::from_str("file:disk.img;direct;create=1G").unwrap();
2287 assert!(matches!(
2288 &disk,
2289 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
2290 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
2291 ));
2292
2293 let disk = DiskCliKind::from_str("file:disk.img;create=1G;direct").unwrap();
2294 assert!(matches!(
2295 &disk,
2296 DiskCliKind::File { path, create_with_len: Some(len), direct: true }
2297 if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
2298 ));
2299
2300 let disk = DiskCliKind::from_str("file:disk.img").unwrap();
2302 assert!(matches!(
2303 &disk,
2304 DiskCliKind::File { path, create_with_len: None, direct: false }
2305 if path == Path::new("disk.img")
2306 ));
2307
2308 assert!(DiskCliKind::from_str("file:disk.img;bogus").is_err());
2310
2311 assert!(DiskCliKind::from_str("sql:db.sqlite;direct").is_err());
2313 }
2314
2315 #[test]
2316 fn test_parse_memory_disk() {
2317 let s = "mem:1G";
2318 let disk = DiskCliKind::from_str(s).unwrap();
2319 match disk {
2320 DiskCliKind::Memory(size) => {
2321 assert_eq!(size, 1024 * 1024 * 1024); }
2323 _ => panic!("Expected Memory variant"),
2324 }
2325 }
2326
2327 #[test]
2328 fn test_parse_pcie_disk() {
2329 assert_eq!(
2330 DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
2331 Some("p0".to_string())
2332 );
2333 assert_eq!(
2334 DiskCli::from_str("file:path.vhdx,pcie_port=p0")
2335 .unwrap()
2336 .pcie_port,
2337 Some("p0".to_string())
2338 );
2339 assert_eq!(
2340 DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
2341 .unwrap()
2342 .pcie_port,
2343 Some("p0".to_string())
2344 );
2345
2346 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
2348
2349 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
2351 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
2352 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
2353 }
2354
2355 #[test]
2356 fn test_parse_memory_diff_disk() {
2357 let s = "memdiff:file:base.img";
2358 let disk = DiskCliKind::from_str(s).unwrap();
2359 match disk {
2360 DiskCliKind::MemoryDiff(inner) => match *inner {
2361 DiskCliKind::File {
2362 path,
2363 create_with_len,
2364 ..
2365 } => {
2366 assert_eq!(path, PathBuf::from("base.img"));
2367 assert_eq!(create_with_len, None);
2368 }
2369 _ => panic!("Expected File variant inside MemoryDiff"),
2370 },
2371 _ => panic!("Expected MemoryDiff variant"),
2372 }
2373 }
2374
2375 #[test]
2376 fn test_parse_sqlite_disk() {
2377 let s = "sql:db.sqlite;create=2G";
2378 let disk = DiskCliKind::from_str(s).unwrap();
2379 match disk {
2380 DiskCliKind::Sqlite {
2381 path,
2382 create_with_len,
2383 } => {
2384 assert_eq!(path, PathBuf::from("db.sqlite"));
2385 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
2386 }
2387 _ => panic!("Expected Sqlite variant"),
2388 }
2389
2390 let s = "sql:db.sqlite";
2392 let disk = DiskCliKind::from_str(s).unwrap();
2393 match disk {
2394 DiskCliKind::Sqlite {
2395 path,
2396 create_with_len,
2397 } => {
2398 assert_eq!(path, PathBuf::from("db.sqlite"));
2399 assert_eq!(create_with_len, None);
2400 }
2401 _ => panic!("Expected Sqlite variant"),
2402 }
2403 }
2404
2405 #[test]
2406 fn test_parse_sqlite_diff_disk() {
2407 let s = "sqldiff:diff.sqlite;create:file:base.img";
2409 let disk = DiskCliKind::from_str(s).unwrap();
2410 match disk {
2411 DiskCliKind::SqliteDiff { path, create, disk } => {
2412 assert_eq!(path, PathBuf::from("diff.sqlite"));
2413 assert!(create);
2414 match *disk {
2415 DiskCliKind::File {
2416 path,
2417 create_with_len,
2418 ..
2419 } => {
2420 assert_eq!(path, PathBuf::from("base.img"));
2421 assert_eq!(create_with_len, None);
2422 }
2423 _ => panic!("Expected File variant inside SqliteDiff"),
2424 }
2425 }
2426 _ => panic!("Expected SqliteDiff variant"),
2427 }
2428
2429 let s = "sqldiff:diff.sqlite:file:base.img";
2431 let disk = DiskCliKind::from_str(s).unwrap();
2432 match disk {
2433 DiskCliKind::SqliteDiff { path, create, disk } => {
2434 assert_eq!(path, PathBuf::from("diff.sqlite"));
2435 assert!(!create);
2436 match *disk {
2437 DiskCliKind::File {
2438 path,
2439 create_with_len,
2440 ..
2441 } => {
2442 assert_eq!(path, PathBuf::from("base.img"));
2443 assert_eq!(create_with_len, None);
2444 }
2445 _ => panic!("Expected File variant inside SqliteDiff"),
2446 }
2447 }
2448 _ => panic!("Expected SqliteDiff variant"),
2449 }
2450 }
2451
2452 #[test]
2453 fn test_parse_autocache_sqlite_disk() {
2454 let disk =
2456 DiskCliKind::parse_autocache(":file:disk.vhd", Ok("/tmp/cache".to_string())).unwrap();
2457 assert!(matches!(
2458 disk,
2459 DiskCliKind::AutoCacheSqlite {
2460 cache_path,
2461 key,
2462 disk: _disk,
2463 } if cache_path == "/tmp/cache" && key.is_none()
2464 ));
2465
2466 let disk =
2468 DiskCliKind::parse_autocache("mykey:file:disk.vhd", Ok("/tmp/cache".to_string()))
2469 .unwrap();
2470 assert!(matches!(
2471 disk,
2472 DiskCliKind::AutoCacheSqlite {
2473 cache_path,
2474 key: Some(key),
2475 disk: _disk,
2476 } if cache_path == "/tmp/cache" && key == "mykey"
2477 ));
2478
2479 assert!(
2481 DiskCliKind::parse_autocache(":file:disk.vhd", Err(std::env::VarError::NotPresent),)
2482 .is_err()
2483 );
2484 }
2485
2486 #[test]
2487 fn test_parse_disk_errors() {
2488 assert!(DiskCliKind::from_str("invalid:").is_err());
2489 assert!(DiskCliKind::from_str("memory:extra").is_err());
2490
2491 assert!(DiskCliKind::from_str("sqlite:").is_err());
2493 }
2494
2495 #[test]
2496 fn test_parse_errors() {
2497 assert!(DiskCliKind::from_str("mem:invalid").is_err());
2499
2500 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
2502
2503 assert!(
2505 DiskCliKind::parse_autocache("key:file:disk.vhd", Err(std::env::VarError::NotPresent),)
2506 .is_err()
2507 );
2508
2509 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
2511
2512 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
2514
2515 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
2517
2518 assert!(DiskCliKind::from_str("invalid:path").is_err());
2520
2521 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
2523 }
2524
2525 #[test]
2526 fn test_fs_args_from_str() {
2527 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
2528 assert_eq!(args.tag, "tag1");
2529 assert_eq!(args.path, "/path/to/fs");
2530
2531 assert!(FsArgs::from_str("tag1").is_err());
2533 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
2534 }
2535
2536 #[test]
2537 fn test_fs_args_with_options_from_str() {
2538 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
2539 assert_eq!(args.tag, "tag1");
2540 assert_eq!(args.path, "/path/to/fs");
2541 assert_eq!(args.options, "opt1;opt2");
2542
2543 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
2545 assert_eq!(args.tag, "tag1");
2546 assert_eq!(args.path, "/path/to/fs");
2547 assert_eq!(args.options, "");
2548
2549 assert!(FsArgsWithOptions::from_str("tag1").is_err());
2551 }
2552
2553 #[test]
2554 fn test_serial_config_from_str() {
2555 assert_eq!(
2556 SerialConfigCli::from_str("none").unwrap(),
2557 SerialConfigCli::None
2558 );
2559 assert_eq!(
2560 SerialConfigCli::from_str("console").unwrap(),
2561 SerialConfigCli::Console
2562 );
2563 assert_eq!(
2564 SerialConfigCli::from_str("stderr").unwrap(),
2565 SerialConfigCli::Stderr
2566 );
2567
2568 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
2570 if let SerialConfigCli::File(path) = file_config {
2571 assert_eq!(path.to_str().unwrap(), "/path/to/file");
2572 } else {
2573 panic!("Expected File variant");
2574 }
2575
2576 match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
2578 SerialConfigCli::NewConsole(None, Some(name)) => {
2579 assert_eq!(name, "MyTerm");
2580 }
2581 _ => panic!("Expected NewConsole variant with name"),
2582 }
2583
2584 match SerialConfigCli::from_str("term").unwrap() {
2586 SerialConfigCli::NewConsole(None, None) => (),
2587 _ => panic!("Expected NewConsole variant without name"),
2588 }
2589
2590 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
2592 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
2593 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
2594 assert_eq!(name, "MyTerm");
2595 }
2596 _ => panic!("Expected NewConsole variant with name"),
2597 }
2598
2599 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
2601 SerialConfigCli::NewConsole(Some(path), None) => {
2602 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
2603 }
2604 _ => panic!("Expected NewConsole variant without name"),
2605 }
2606
2607 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
2609 SerialConfigCli::Tcp(addr) => {
2610 assert_eq!(addr.to_string(), "127.0.0.1:1234");
2611 }
2612 _ => panic!("Expected Tcp variant"),
2613 }
2614
2615 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
2617 SerialConfigCli::Pipe(path) => {
2618 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
2619 }
2620 _ => panic!("Expected Pipe variant"),
2621 }
2622
2623 assert!(SerialConfigCli::from_str("").is_err());
2625 assert!(SerialConfigCli::from_str("unknown").is_err());
2626 assert!(SerialConfigCli::from_str("file").is_err());
2627 assert!(SerialConfigCli::from_str("listen").is_err());
2628 }
2629
2630 #[test]
2631 fn test_endpoint_config_from_str() {
2632 assert!(matches!(
2634 EndpointConfigCli::from_str("none").unwrap(),
2635 EndpointConfigCli::None
2636 ));
2637
2638 match EndpointConfigCli::from_str("consomme").unwrap() {
2640 EndpointConfigCli::Consomme { cidr: None } => (),
2641 _ => panic!("Expected Consomme variant without cidr"),
2642 }
2643
2644 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
2646 EndpointConfigCli::Consomme { cidr: Some(cidr) } => {
2647 assert_eq!(cidr, "192.168.0.0/24");
2648 }
2649 _ => panic!("Expected Consomme variant with cidr"),
2650 }
2651
2652 match EndpointConfigCli::from_str("dio").unwrap() {
2654 EndpointConfigCli::Dio { id: None } => (),
2655 _ => panic!("Expected Dio variant without id"),
2656 }
2657
2658 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
2660 EndpointConfigCli::Dio { id: Some(id) } => {
2661 assert_eq!(id, "test_id");
2662 }
2663 _ => panic!("Expected Dio variant with id"),
2664 }
2665
2666 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
2668 EndpointConfigCli::Tap { name } => {
2669 assert_eq!(name, "tap0");
2670 }
2671 _ => panic!("Expected Tap variant"),
2672 }
2673
2674 assert!(EndpointConfigCli::from_str("invalid").is_err());
2676 }
2677
2678 #[test]
2679 fn test_nic_config_from_str() {
2680 use openvmm_defs::config::DeviceVtl;
2681
2682 let config = NicConfigCli::from_str("none").unwrap();
2684 assert_eq!(config.vtl, DeviceVtl::Vtl0);
2685 assert!(config.max_queues.is_none());
2686 assert!(!config.underhill);
2687 assert!(config.pcie_port.is_none());
2688 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2689
2690 let config = NicConfigCli::from_str("vtl2:none").unwrap();
2692 assert_eq!(config.vtl, DeviceVtl::Vtl2);
2693 assert!(config.pcie_port.is_none());
2694 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2695
2696 let config = NicConfigCli::from_str("queues=4:none").unwrap();
2698 assert_eq!(config.max_queues, Some(4));
2699 assert!(config.pcie_port.is_none());
2700 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2701
2702 let config = NicConfigCli::from_str("uh:none").unwrap();
2704 assert!(config.underhill);
2705 assert!(config.pcie_port.is_none());
2706 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2707
2708 let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
2710 assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
2711 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2712
2713 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
2715 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
2717 assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
2718 assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
2719 assert!(NicConfigCli::from_str("pcie_port:none").is_err());
2720 }
2721
2722 #[test]
2723 fn test_parse_pcie_port_prefix() {
2724 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
2726 assert_eq!(port.unwrap(), "rp0");
2727 assert_eq!(rest, "tag,path");
2728
2729 let (port, rest) = parse_pcie_port_prefix("tag,path");
2731 assert!(port.is_none());
2732 assert_eq!(rest, "tag,path");
2733
2734 let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
2736 assert!(port.is_none());
2737 assert_eq!(rest, "pcie_port=:tag,path");
2738
2739 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
2741 assert!(port.is_none());
2742 assert_eq!(rest, "pcie_port=rp0");
2743 }
2744
2745 #[test]
2746 fn test_fs_args_pcie_port() {
2747 let args = FsArgs::from_str("myfs,/path").unwrap();
2749 assert_eq!(args.tag, "myfs");
2750 assert_eq!(args.path, "/path");
2751 assert!(args.pcie_port.is_none());
2752
2753 let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
2755 assert_eq!(args.pcie_port.unwrap(), "rp0");
2756 assert_eq!(args.tag, "myfs");
2757 assert_eq!(args.path, "/path");
2758
2759 assert!(FsArgs::from_str("myfs").is_err());
2761 assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
2762 }
2763
2764 #[test]
2765 fn test_fs_args_with_options_pcie_port() {
2766 let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
2768 assert_eq!(args.tag, "myfs");
2769 assert_eq!(args.path, "/path");
2770 assert_eq!(args.options, "uid=1000");
2771 assert!(args.pcie_port.is_none());
2772
2773 let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
2775 assert_eq!(args.pcie_port.unwrap(), "rp0");
2776 assert_eq!(args.tag, "myfs");
2777 assert_eq!(args.path, "/path");
2778 assert_eq!(args.options, "uid=1000");
2779
2780 assert!(FsArgsWithOptions::from_str("myfs").is_err());
2782 }
2783
2784 #[test]
2785 fn test_virtio_pmem_args_pcie_port() {
2786 let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
2788 assert_eq!(args.path, "/path/to/file");
2789 assert!(args.pcie_port.is_none());
2790
2791 let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
2793 assert_eq!(args.pcie_port.unwrap(), "rp0");
2794 assert_eq!(args.path, "/path/to/file");
2795
2796 assert!(VirtioPmemArgs::from_str("").is_err());
2798 assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
2799 }
2800
2801 #[test]
2802 fn test_smt_config_from_str() {
2803 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
2804 assert_eq!(
2805 SmtConfigCli::from_str("force").unwrap(),
2806 SmtConfigCli::Force
2807 );
2808 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
2809
2810 assert!(SmtConfigCli::from_str("invalid").is_err());
2812 assert!(SmtConfigCli::from_str("").is_err());
2813 }
2814
2815 #[test]
2816 fn test_pcat_boot_order_from_str() {
2817 let order = PcatBootOrderCli::from_str("optical").unwrap();
2819 assert_eq!(order.0[0], PcatBootDevice::Optical);
2820
2821 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
2823 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
2824 assert_eq!(order.0[1], PcatBootDevice::Network);
2825
2826 assert!(PcatBootOrderCli::from_str("invalid").is_err());
2828 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
2830
2831 #[test]
2832 fn test_floppy_disk_from_str() {
2833 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
2835 assert!(!disk.read_only);
2836 match disk.kind {
2837 DiskCliKind::File {
2838 path,
2839 create_with_len,
2840 ..
2841 } => {
2842 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
2843 assert_eq!(create_with_len, None);
2844 }
2845 _ => panic!("Expected File variant"),
2846 }
2847
2848 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
2850 assert!(disk.read_only);
2851
2852 assert!(FloppyDiskCli::from_str("").is_err());
2854 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
2855 }
2856
2857 #[test]
2858 fn test_pcie_root_complex_from_str() {
2859 const ONE_MB: u64 = 1024 * 1024;
2860 const ONE_GB: u64 = 1024 * ONE_MB;
2861
2862 const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
2863 const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
2864
2865 assert_eq!(
2866 PcieRootComplexCli::from_str("rc0").unwrap(),
2867 PcieRootComplexCli {
2868 name: "rc0".to_string(),
2869 segment: 0,
2870 start_bus: 0,
2871 end_bus: 255,
2872 low_mmio: DEFAULT_LOW_MMIO,
2873 high_mmio: DEFAULT_HIGH_MMIO,
2874 }
2875 );
2876
2877 assert_eq!(
2878 PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
2879 PcieRootComplexCli {
2880 name: "rc1".to_string(),
2881 segment: 1,
2882 start_bus: 0,
2883 end_bus: 255,
2884 low_mmio: DEFAULT_LOW_MMIO,
2885 high_mmio: DEFAULT_HIGH_MMIO,
2886 }
2887 );
2888
2889 assert_eq!(
2890 PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
2891 PcieRootComplexCli {
2892 name: "rc2".to_string(),
2893 segment: 0,
2894 start_bus: 32,
2895 end_bus: 255,
2896 low_mmio: DEFAULT_LOW_MMIO,
2897 high_mmio: DEFAULT_HIGH_MMIO,
2898 }
2899 );
2900
2901 assert_eq!(
2902 PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
2903 PcieRootComplexCli {
2904 name: "rc3".to_string(),
2905 segment: 0,
2906 start_bus: 0,
2907 end_bus: 31,
2908 low_mmio: DEFAULT_LOW_MMIO,
2909 high_mmio: DEFAULT_HIGH_MMIO,
2910 }
2911 );
2912
2913 assert_eq!(
2914 PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
2915 PcieRootComplexCli {
2916 name: "rc4".to_string(),
2917 segment: 0,
2918 start_bus: 32,
2919 end_bus: 127,
2920 low_mmio: DEFAULT_LOW_MMIO,
2921 high_mmio: 2 * ONE_GB,
2922 }
2923 );
2924
2925 assert_eq!(
2926 PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
2927 PcieRootComplexCli {
2928 name: "rc5".to_string(),
2929 segment: 2,
2930 start_bus: 32,
2931 end_bus: 127,
2932 low_mmio: DEFAULT_LOW_MMIO,
2933 high_mmio: DEFAULT_HIGH_MMIO,
2934 }
2935 );
2936
2937 assert_eq!(
2938 PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
2939 PcieRootComplexCli {
2940 name: "rc6".to_string(),
2941 segment: 0,
2942 start_bus: 0,
2943 end_bus: 255,
2944 low_mmio: ONE_MB as u32,
2945 high_mmio: 64 * ONE_GB,
2946 }
2947 );
2948
2949 assert!(PcieRootComplexCli::from_str("").is_err());
2951 assert!(PcieRootComplexCli::from_str("poorly,").is_err());
2952 assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
2953 assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
2954 assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
2955 assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
2956 assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
2957 assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
2958 assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
2959 assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
2960 assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
2961 }
2962
2963 #[test]
2964 fn test_pcie_root_port_from_str() {
2965 assert_eq!(
2966 PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
2967 PcieRootPortCli {
2968 root_complex_name: "rc0".to_string(),
2969 name: "rc0rp0".to_string(),
2970 hotplug: false,
2971 }
2972 );
2973
2974 assert_eq!(
2975 PcieRootPortCli::from_str("my_rc:port2").unwrap(),
2976 PcieRootPortCli {
2977 root_complex_name: "my_rc".to_string(),
2978 name: "port2".to_string(),
2979 hotplug: false,
2980 }
2981 );
2982
2983 assert_eq!(
2985 PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
2986 PcieRootPortCli {
2987 root_complex_name: "my_rc".to_string(),
2988 name: "port2".to_string(),
2989 hotplug: true,
2990 }
2991 );
2992
2993 assert!(PcieRootPortCli::from_str("").is_err());
2995 assert!(PcieRootPortCli::from_str("rp0").is_err());
2996 assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
2997 assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
2998 assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
2999 }
3000
3001 #[test]
3002 fn test_pcie_switch_from_str() {
3003 assert_eq!(
3004 GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
3005 GenericPcieSwitchCli {
3006 port_name: "rp0".to_string(),
3007 name: "switch0".to_string(),
3008 num_downstream_ports: 4,
3009 hotplug: false,
3010 }
3011 );
3012
3013 assert_eq!(
3014 GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
3015 GenericPcieSwitchCli {
3016 port_name: "port1".to_string(),
3017 name: "my_switch".to_string(),
3018 num_downstream_ports: 4,
3019 hotplug: false,
3020 }
3021 );
3022
3023 assert_eq!(
3024 GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
3025 GenericPcieSwitchCli {
3026 port_name: "rp2".to_string(),
3027 name: "sw".to_string(),
3028 num_downstream_ports: 8,
3029 hotplug: false,
3030 }
3031 );
3032
3033 assert_eq!(
3035 GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
3036 GenericPcieSwitchCli {
3037 port_name: "switch0-downstream-1".to_string(),
3038 name: "child_switch".to_string(),
3039 num_downstream_ports: 4,
3040 hotplug: false,
3041 }
3042 );
3043
3044 assert_eq!(
3046 GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
3047 GenericPcieSwitchCli {
3048 port_name: "rp0".to_string(),
3049 name: "switch0".to_string(),
3050 num_downstream_ports: 4,
3051 hotplug: true,
3052 }
3053 );
3054
3055 assert_eq!(
3057 GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
3058 GenericPcieSwitchCli {
3059 port_name: "rp0".to_string(),
3060 name: "switch0".to_string(),
3061 num_downstream_ports: 8,
3062 hotplug: true,
3063 }
3064 );
3065
3066 assert!(GenericPcieSwitchCli::from_str("").is_err());
3068 assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
3069 assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
3070 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
3071 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
3072 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
3073 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
3074 }
3075
3076 #[test]
3077 fn test_pcie_remote_from_str() {
3078 assert_eq!(
3080 PcieRemoteCli::from_str("rc0rp0").unwrap(),
3081 PcieRemoteCli {
3082 port_name: "rc0rp0".to_string(),
3083 socket_addr: None,
3084 hu: 0,
3085 controller: 0,
3086 }
3087 );
3088
3089 assert_eq!(
3091 PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
3092 PcieRemoteCli {
3093 port_name: "rc0rp0".to_string(),
3094 socket_addr: Some("localhost:22567".to_string()),
3095 hu: 0,
3096 controller: 0,
3097 }
3098 );
3099
3100 assert_eq!(
3102 PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
3103 PcieRemoteCli {
3104 port_name: "myport".to_string(),
3105 socket_addr: Some("localhost:22568".to_string()),
3106 hu: 1,
3107 controller: 2,
3108 }
3109 );
3110
3111 assert_eq!(
3113 PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
3114 PcieRemoteCli {
3115 port_name: "port0".to_string(),
3116 socket_addr: None,
3117 hu: 5,
3118 controller: 3,
3119 }
3120 );
3121
3122 assert!(PcieRemoteCli::from_str("").is_err());
3124 assert!(PcieRemoteCli::from_str("port,socket=").is_err());
3125 assert!(PcieRemoteCli::from_str("port,hu=").is_err());
3126 assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
3127 assert!(PcieRemoteCli::from_str("port,controller=").is_err());
3128 assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
3129 assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
3130 }
3131
3132 #[test]
3133 fn test_pidfile_option_parsed() {
3134 let opt = Options::try_parse_from(["openvmm", "--pidfile", "/tmp/test.pid"]).unwrap();
3135 assert_eq!(opt.pidfile, Some(PathBuf::from("/tmp/test.pid")));
3136 }
3137}