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)]
146 pub no_enlightenments: bool,
147
148 #[clap(long)]
150 pub user_mode_apic: bool,
151
152 #[clap(long_help = r#"
154e.g: --disk memdiff:file:/path/to/disk.vhd
155
156syntax: <path> | kind:<arg>[,flag,opt=arg,...]
157
158valid disk kinds:
159 `mem:<len>` memory backed disk
160 <len>: length of ramdisk, e.g.: `1G`
161 `memdiff:<disk>` memory backed diff disk
162 <disk>: lower disk, e.g.: `file:base.img`
163 `file:<path>[;create=<len>]` file-backed disk
164 <path>: path to file
165 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
166 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
167 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
168 `blob:<type>:<url>` HTTP blob (read-only)
169 <type>: `flat` or `vhd1`
170 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
171 <cipher>: `xts-aes-256`
172 `prwrap:<disk>` persistent reservations wrapper
173
174flags:
175 `ro` open disk as read-only
176 `dvd` specifies that device is cd/dvd and it is read_only
177 `vtl2` assign this disk to VTL2
178 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
179 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
180
181options:
182 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
183"#)]
184 #[clap(long, value_name = "FILE")]
185 pub disk: Vec<DiskCli>,
186
187 #[clap(long_help = r#"
189e.g: --nvme memdiff:file:/path/to/disk.vhd
190
191syntax: <path> | kind:<arg>[,flag,opt=arg,...]
192
193valid disk kinds:
194 `mem:<len>` memory backed disk
195 <len>: length of ramdisk, e.g.: `1G`
196 `memdiff:<disk>` memory backed diff disk
197 <disk>: lower disk, e.g.: `file:base.img`
198 `file:<path>[;create=<len>]` file-backed disk
199 <path>: path to file
200 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
201 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
202 `autocache:<key>:<disk>` auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
203 `blob:<type>:<url>` HTTP blob (read-only)
204 <type>: `flat` or `vhd1`
205 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
206 <cipher>: `xts-aes-256`
207 `prwrap:<disk>` persistent reservations wrapper
208
209flags:
210 `ro` open disk as read-only
211 `vtl2` assign this disk to VTL2
212 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
213 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
214
215options:
216 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
217"#)]
218 #[clap(long)]
219 pub nvme: Vec<DiskCli>,
220
221 #[clap(long_help = r#"
223e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
224
225syntax: <path> | kind:<arg>[,flag,opt=arg,...]
226
227valid disk kinds:
228 `mem:<len>` memory backed disk
229 <len>: length of ramdisk, e.g.: `1G`
230 `memdiff:<disk>` memory backed diff disk
231 <disk>: lower disk, e.g.: `file:base.img`
232 `file:<path>` file-backed disk
233 <path>: path to file
234
235flags:
236 `ro` open disk as read-only
237
238options:
239 `pcie_port=<name>` present the disk using pcie under the specified port
240"#)]
241 #[clap(long = "virtio-blk")]
242 pub virtio_blk: Vec<DiskCli>,
243
244 #[clap(long, value_name = "COUNT", default_value = "0")]
246 pub scsi_sub_channels: u16,
247
248 #[clap(long)]
250 pub nic: bool,
251
252 #[clap(long)]
258 pub net: Vec<NicConfigCli>,
259
260 #[clap(long, value_name = "SWITCH_ID")]
264 pub kernel_vmnic: Vec<String>,
265
266 #[clap(long)]
268 pub gfx: bool,
269
270 #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
272 pub vtl2_gfx: bool,
273
274 #[clap(long)]
276 pub vnc: bool,
277
278 #[clap(long, value_name = "PORT", default_value = "5900")]
280 pub vnc_port: u16,
281
282 #[cfg(guest_arch = "x86_64")]
284 #[clap(long, default_value_t)]
285 pub apic_id_offset: u32,
286
287 #[clap(long)]
289 pub vps_per_socket: Option<u32>,
290
291 #[clap(long, default_value = "auto")]
293 pub smt: SmtConfigCli,
294
295 #[cfg(guest_arch = "x86_64")]
297 #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
298 pub x2apic: X2ApicConfig,
299
300 #[clap(long, value_name = "SERIAL")]
302 pub com1: Option<SerialConfigCli>,
303
304 #[clap(long, value_name = "SERIAL")]
306 pub com2: Option<SerialConfigCli>,
307
308 #[clap(long, value_name = "SERIAL")]
310 pub com3: Option<SerialConfigCli>,
311
312 #[clap(long, value_name = "SERIAL")]
314 pub com4: Option<SerialConfigCli>,
315
316 #[structopt(long, value_name = "SERIAL")]
318 pub vmbus_com1_serial: Option<SerialConfigCli>,
319
320 #[structopt(long, value_name = "SERIAL")]
322 pub vmbus_com2_serial: Option<SerialConfigCli>,
323
324 #[clap(long)]
326 pub serial_tx_only: bool,
327
328 #[clap(long, value_name = "SERIAL")]
330 pub debugcon: Option<DebugconSerialConfigCli>,
331
332 #[clap(long, short = 'e')]
334 pub uefi: bool,
335
336 #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
338 pub uefi_firmware: OptionalPathBuf,
339
340 #[clap(long, requires("uefi"))]
342 pub uefi_debug: bool,
343
344 #[clap(long, requires("uefi"))]
346 pub uefi_enable_memory_protections: bool,
347
348 #[clap(long, requires("pcat"))]
359 pub pcat_boot_order: Option<PcatBootOrderCli>,
360
361 #[clap(long, conflicts_with("uefi"))]
363 pub pcat: bool,
364
365 #[clap(long, requires("pcat"), value_name = "FILE")]
367 pub pcat_firmware: Option<PathBuf>,
368
369 #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
371 pub igvm: Option<PathBuf>,
372
373 #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
376 pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
377
378 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
383 pub virtio_9p: Vec<FsArgs>,
384
385 #[clap(long)]
387 pub virtio_9p_debug: bool,
388
389 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
394 pub virtio_fs: Vec<FsArgsWithOptions>,
395
396 #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
401 pub virtio_fs_shmem: Vec<FsArgs>,
402
403 #[clap(long, value_name = "BUS", default_value = "auto")]
405 pub virtio_fs_bus: VirtioBusCli,
406
407 #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
412 pub virtio_pmem: Option<VirtioPmemArgs>,
413
414 #[clap(long)]
416 pub virtio_rng: bool,
417
418 #[clap(long, value_name = "BUS", default_value = "auto")]
420 pub virtio_rng_bus: VirtioBusCli,
421
422 #[clap(long, value_name = "PORT", requires("virtio_rng"))]
424 pub virtio_rng_pcie_port: Option<String>,
425
426 #[clap(long)]
432 pub virtio_console: Option<SerialConfigCli>,
433
434 #[clap(long, value_name = "PORT", requires("virtio_console"))]
436 pub virtio_console_pcie_port: Option<String>,
437
438 #[clap(long, value_name = "PATH")]
440 pub virtio_vsock_path: Option<String>,
441
442 #[clap(long)]
449 pub virtio_net: Vec<NicConfigCli>,
450
451 #[clap(long, value_name = "PATH")]
453 pub log_file: Option<PathBuf>,
454
455 #[clap(long, value_name = "SOCKETPATH")]
457 pub ttrpc: Option<PathBuf>,
458
459 #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
461 pub grpc: Option<PathBuf>,
462
463 #[clap(long)]
465 pub single_process: bool,
466
467 #[cfg(windows)]
469 #[clap(long, value_name = "PATH")]
470 pub device: Vec<String>,
471
472 #[clap(long, requires("uefi"))]
474 pub disable_frontpage: bool,
475
476 #[clap(long)]
478 pub tpm: bool,
479
480 #[clap(long, default_value = "control", hide(true))]
484 #[expect(clippy::option_option)]
485 pub internal_worker: Option<Option<String>>,
486
487 #[clap(long, requires("vtl2"))]
489 pub vmbus_redirect: bool,
490
491 #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
493 pub vmbus_max_version: Option<u32>,
494
495 #[clap(long_help = r#"
499e.g: --vmgs memdiff:file:/path/to/file.vmgs
500
501syntax: <path> | kind:<arg>[,flag]
502
503valid disk kinds:
504 `mem:<len>` memory backed disk
505 <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
506 `memdiff:<disk>[;create=<len>]` memory backed diff disk
507 <disk>: lower disk, e.g.: `file:base.img`
508 `file:<path>` file-backed disk
509 <path>: path to file
510
511flags:
512 `fmt` reprovision the VMGS before boot
513 `fmt-on-fail` reprovision the VMGS before boot if it is corrupted
514"#)]
515 #[clap(long)]
516 pub vmgs: Option<VmgsCli>,
517
518 #[clap(long, requires("vmgs"))]
520 pub test_gsp_by_id: bool,
521
522 #[clap(long, requires("pcat"), value_name = "FILE")]
524 pub vga_firmware: Option<PathBuf>,
525
526 #[clap(long)]
528 pub secure_boot: bool,
529
530 #[clap(long)]
532 pub secure_boot_template: Option<SecureBootTemplateCli>,
533
534 #[clap(long, value_name = "PATH")]
536 pub custom_uefi_json: Option<PathBuf>,
537
538 #[clap(long, hide(true))]
543 pub relay_console_path: Option<PathBuf>,
544
545 #[clap(long, hide(true))]
549 pub relay_console_title: Option<String>,
550
551 #[clap(long, value_name = "PORT")]
553 pub gdb: Option<u16>,
554
555 #[clap(long)]
560 pub mana: Vec<NicConfigCli>,
561
562 #[clap(long)]
564 pub hypervisor: Option<String>,
565
566 #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
574 pub custom_dsdt: Option<PathBuf>,
575
576 #[clap(long_help = r#"
586e.g: --ide memdiff:file:/path/to/disk.vhd
587
588syntax: <path> | kind:<arg>[,flag,opt=arg,...]
589
590valid disk kinds:
591 `mem:<len>` memory backed disk
592 <len>: length of ramdisk, e.g.: `1G`
593 `memdiff:<disk>` memory backed diff disk
594 <disk>: lower disk, e.g.: `file:base.img`
595 `file:<path>[;create=<len>]` file-backed disk
596 <path>: path to file
597 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
598 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
599 `blob:<type>:<url>` HTTP blob (read-only)
600 <type>: `flat` or `vhd1`
601 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
602 <cipher>: `xts-aes-256`
603
604additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
605this list is not exhaustive.
606
607flags:
608 `ro` open disk as read-only
609 `s` attach drive to secondary ide channel
610 `dvd` specifies that device is cd/dvd and it is read_only
611"#)]
612 #[clap(long, value_name = "FILE", requires("pcat"))]
613 pub ide: Vec<IdeDiskCli>,
614
615 #[clap(long_help = r#"
618e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
619
620syntax: <path> | kind:<arg>[,flag,opt=arg,...]
621
622valid disk kinds:
623 `mem:<len>` memory backed disk
624 <len>: length of ramdisk, e.g.: `1G`
625 `memdiff:<disk>` memory backed diff disk
626 <disk>: lower disk, e.g.: `file:base.img`
627 `file:<path>[;create=<len>]` file-backed disk
628 <path>: path to file
629 `sql:<path>[;create=<len>]` SQLite-backed disk (dev/test)
630 `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
631 `blob:<type>:<url>` HTTP blob (read-only)
632 <type>: `flat` or `vhd1`
633 `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
634 <cipher>: `xts-aes-256`
635
636flags:
637 `ro` open disk as read-only
638"#)]
639 #[clap(long, value_name = "FILE", requires("pcat"))]
640 pub floppy: Vec<FloppyDiskCli>,
641
642 #[clap(long)]
644 pub guest_watchdog: bool,
645
646 #[clap(long)]
648 pub openhcl_dump_path: Option<PathBuf>,
649
650 #[clap(long)]
652 pub halt_on_reset: bool,
653
654 #[clap(long)]
656 pub write_saved_state_proto: Option<PathBuf>,
657
658 #[clap(long)]
660 pub imc: Option<PathBuf>,
661
662 #[clap(long)]
664 pub mcr: bool, #[clap(long)]
668 pub battery: bool,
669
670 #[clap(long)]
672 pub uefi_console_mode: Option<UefiConsoleModeCli>,
673
674 #[clap(long_help = r#"
676Set the EFI diagnostics log level.
677
678options:
679 default default (ERROR and WARN only)
680 info info (ERROR, WARN, and INFO)
681 full full (all log levels)
682"#)]
683 #[clap(long, requires("uefi"))]
684 pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
685
686 #[clap(long)]
688 pub default_boot_always_attempt: bool,
689
690 #[clap(long_help = r#"
692Attach root complexes to the VM.
693
694Examples:
695 # Attach root complex rc0 on segment 0 with bus and MMIO ranges
696 --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
697
698Syntax: <name>[,opt=arg,...]
699
700Options:
701 `segment=<value>` configures the PCI Express segment, default 0
702 `start_bus=<value>` lowest valid bus number, default 0
703 `end_bus=<value>` highest valid bus number, default 255
704 `low_mmio=<size>` low MMIO window size, default 64M
705 `high_mmio=<size>` high MMIO window size, default 1G
706"#)]
707 #[clap(long, conflicts_with("pcat"))]
708 pub pcie_root_complex: Vec<PcieRootComplexCli>,
709
710 #[clap(long_help = r#"
712Attach root ports to root complexes.
713
714Examples:
715 # Attach root port rc0rp0 to root complex rc0
716 --pcie-root-port rc0:rc0rp0
717
718 # Attach root port rc0rp1 to root complex rc0 with hotplug support
719 --pcie-root-port rc0:rc0rp1,hotplug
720
721Syntax: <root_complex_name>:<name>[,hotplug]
722
723Options:
724 `hotplug` enable hotplug support for this root port
725"#)]
726 #[clap(long, conflicts_with("pcat"))]
727 pub pcie_root_port: Vec<PcieRootPortCli>,
728
729 #[clap(long_help = r#"
731Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
732
733Examples:
734 # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
735 --pcie-switch rp0:switch0,num_downstream_ports=4
736
737 # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
738 --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
739
740 # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
741 --pcie-switch rp0:switch0
742 --pcie-switch switch0-downstream-0:switch1
743 --pcie-switch switch1-downstream-1:switch2
744
745 # Enable hotplug on all downstream switch ports of switch0
746 --pcie-switch rp0:switch0,hotplug
747
748Syntax: <port_name>:<name>[,opt,opt=arg,...]
749
750 port_name can be:
751 - Root port name (e.g., "rp0") to connect directly to a root port
752 - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
753
754Options:
755 `hotplug` enable hotplug support for all downstream switch ports
756 `num_downstream_ports=<value>` number of downstream ports, default 4
757"#)]
758 #[clap(long, conflicts_with("pcat"))]
759 pub pcie_switch: Vec<GenericPcieSwitchCli>,
760
761 #[clap(long_help = r#"
763Attach PCIe devices to root ports or downstream switch ports
764which are implemented in a simulator running in a remote process.
765
766Examples:
767 # Attach to root port rc0rp0 with default socket
768 --pcie-remote rc0rp0
769
770 # Attach with custom socket address
771 --pcie-remote rc0rp0,socket=0.0.0.0:48914
772
773 # Specify HU and controller identifiers
774 --pcie-remote rc0rp0,hu=1,controller=0
775
776 # Multiple devices on different ports
777 --pcie-remote rc0rp0,socket=0.0.0.0:48914
778 --pcie-remote rc0rp1,socket=0.0.0.0:48915
779
780Syntax: <port_name>[,opt=arg,...]
781
782Options:
783 `socket=<address>` TCP socket (default: localhost:48914)
784 `hu=<value>` Hardware unit identifier (default: 0)
785 `controller=<value>` Controller identifier (default: 0)
786"#)]
787 #[clap(long, conflicts_with("pcat"))]
788 pub pcie_remote: Vec<PcieRemoteCli>,
789}
790
791#[derive(Clone, Debug, PartialEq)]
792pub struct FsArgs {
793 pub tag: String,
794 pub path: String,
795 pub pcie_port: Option<String>,
796}
797
798impl FromStr for FsArgs {
799 type Err = anyhow::Error;
800
801 fn from_str(s: &str) -> Result<Self, Self::Err> {
802 let (pcie_port, s) = parse_pcie_port_prefix(s);
803 let mut s = s.split(',');
804 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
805 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
806 };
807 Ok(Self {
808 tag: tag.to_owned(),
809 path: path.to_owned(),
810 pcie_port,
811 })
812 }
813}
814
815#[derive(Clone, Debug, PartialEq)]
816pub struct FsArgsWithOptions {
817 pub tag: String,
819 pub path: String,
821 pub options: String,
823 pub pcie_port: Option<String>,
825}
826
827impl FromStr for FsArgsWithOptions {
828 type Err = anyhow::Error;
829
830 fn from_str(s: &str) -> Result<Self, Self::Err> {
831 let (pcie_port, s) = parse_pcie_port_prefix(s);
832 let mut s = s.split(',');
833 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
834 anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
835 };
836 let options = s.collect::<Vec<_>>().join(";");
837 Ok(Self {
838 tag: tag.to_owned(),
839 path: path.to_owned(),
840 options,
841 pcie_port,
842 })
843 }
844}
845
846#[derive(Copy, Clone, clap::ValueEnum)]
847pub enum VirtioBusCli {
848 Auto,
849 Mmio,
850 Pci,
851 Vpci,
852}
853
854fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
859 if let Some(rest) = s.strip_prefix("pcie_port=") {
860 if let Some((port, rest)) = rest.split_once(':') {
861 if !port.is_empty() {
862 return (Some(port.to_string()), rest);
863 }
864 }
865 }
866 (None, s)
867}
868
869#[derive(Clone, Debug, PartialEq)]
870pub struct VirtioPmemArgs {
871 pub path: String,
872 pub pcie_port: Option<String>,
873}
874
875impl FromStr for VirtioPmemArgs {
876 type Err = anyhow::Error;
877
878 fn from_str(s: &str) -> Result<Self, Self::Err> {
879 let (pcie_port, s) = parse_pcie_port_prefix(s);
880 if s.is_empty() {
881 anyhow::bail!("expected [pcie_port=<port>:]<path>");
882 }
883 Ok(Self {
884 path: s.to_owned(),
885 pcie_port,
886 })
887 }
888}
889
890#[derive(clap::ValueEnum, Clone, Copy)]
891pub enum SecureBootTemplateCli {
892 Windows,
893 UefiCa,
894}
895
896fn parse_memory(s: &str) -> anyhow::Result<u64> {
897 if s == "VMGS_DEFAULT" {
898 Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
899 } else {
900 || -> Option<u64> {
901 let mut b = s.as_bytes();
902 if s.ends_with('B') {
903 b = &b[..b.len() - 1]
904 }
905 if b.is_empty() {
906 return None;
907 }
908 let multi = match b[b.len() - 1] as char {
909 'T' => Some(1024 * 1024 * 1024 * 1024),
910 'G' => Some(1024 * 1024 * 1024),
911 'M' => Some(1024 * 1024),
912 'K' => Some(1024),
913 _ => None,
914 };
915 if multi.is_some() {
916 b = &b[..b.len() - 1]
917 }
918 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
919 Some(n * multi.unwrap_or(1))
920 }()
921 .with_context(|| format!("invalid memory size '{0}'", s))
922 }
923}
924
925fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
927 match s.strip_prefix("0x") {
928 Some(rest) => u64::from_str_radix(rest, 16),
929 None => s.parse::<u64>(),
930 }
931}
932
933#[derive(Clone, Debug, PartialEq)]
934pub enum DiskCliKind {
935 Memory(u64),
937 MemoryDiff(Box<DiskCliKind>),
939 Sqlite {
941 path: PathBuf,
942 create_with_len: Option<u64>,
943 },
944 SqliteDiff {
946 path: PathBuf,
947 create: bool,
948 disk: Box<DiskCliKind>,
949 },
950 AutoCacheSqlite {
952 cache_path: String,
953 key: Option<String>,
954 disk: Box<DiskCliKind>,
955 },
956 PersistentReservationsWrapper(Box<DiskCliKind>),
958 File {
960 path: PathBuf,
961 create_with_len: Option<u64>,
962 },
963 Blob {
965 kind: BlobKind,
966 url: String,
967 },
968 Crypt {
970 cipher: DiskCipher,
971 key_file: PathBuf,
972 disk: Box<DiskCliKind>,
973 },
974 DelayDiskWrapper {
976 delay_ms: u64,
977 disk: Box<DiskCliKind>,
978 },
979}
980
981#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
982pub enum DiskCipher {
983 #[clap(name = "xts-aes-256")]
984 XtsAes256,
985}
986
987#[derive(Copy, Clone, Debug, PartialEq)]
988pub enum BlobKind {
989 Flat,
990 Vhd1,
991}
992
993fn parse_path_and_len(arg: &str) -> anyhow::Result<(PathBuf, Option<u64>)> {
994 Ok(match arg.split_once(';') {
995 Some((path, len)) => {
996 let Some(len) = len.strip_prefix("create=") else {
997 anyhow::bail!("invalid syntax after ';', expected 'create=<len>'")
998 };
999
1000 let len = parse_memory(len)?;
1001
1002 (path.into(), Some(len))
1003 }
1004 None => (arg.into(), None),
1005 })
1006}
1007
1008impl FromStr for DiskCliKind {
1009 type Err = anyhow::Error;
1010
1011 fn from_str(s: &str) -> anyhow::Result<Self> {
1012 let disk = match s.split_once(':') {
1013 None => {
1015 let (path, create_with_len) = parse_path_and_len(s)?;
1016 DiskCliKind::File {
1017 path,
1018 create_with_len,
1019 }
1020 }
1021 Some((kind, arg)) => match kind {
1022 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
1023 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
1024 "sql" => {
1025 let (path, create_with_len) = parse_path_and_len(arg)?;
1026 DiskCliKind::Sqlite {
1027 path,
1028 create_with_len,
1029 }
1030 }
1031 "sqldiff" => {
1032 let (path_and_opts, kind) =
1033 arg.split_once(':').context("expected path[;opts]:kind")?;
1034 let disk = Box::new(kind.parse()?);
1035 match path_and_opts.split_once(';') {
1036 Some((path, create)) => {
1037 if create != "create" {
1038 anyhow::bail!("invalid syntax after ';', expected 'create'")
1039 }
1040 DiskCliKind::SqliteDiff {
1041 path: path.into(),
1042 create: true,
1043 disk,
1044 }
1045 }
1046 None => DiskCliKind::SqliteDiff {
1047 path: path_and_opts.into(),
1048 create: false,
1049 disk,
1050 },
1051 }
1052 }
1053 "autocache" => {
1054 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
1055 let cache_path = std::env::var("OPENVMM_AUTO_CACHE_PATH")
1056 .context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
1057 DiskCliKind::AutoCacheSqlite {
1058 cache_path,
1059 key: (!key.is_empty()).then(|| key.to_string()),
1060 disk: Box::new(kind.parse()?),
1061 }
1062 }
1063 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
1064 "file" => {
1065 let (path, create_with_len) = parse_path_and_len(arg)?;
1066 DiskCliKind::File {
1067 path,
1068 create_with_len,
1069 }
1070 }
1071 "blob" => {
1072 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
1073 let blob_kind = match blob_kind {
1074 "flat" => BlobKind::Flat,
1075 "vhd1" => BlobKind::Vhd1,
1076 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
1077 };
1078 DiskCliKind::Blob {
1079 kind: blob_kind,
1080 url: url.to_string(),
1081 }
1082 }
1083 "crypt" => {
1084 let (cipher, (key, kind)) = arg
1085 .split_once(':')
1086 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
1087 .context("expected cipher:key_file:kind")?;
1088 DiskCliKind::Crypt {
1089 cipher: ValueEnum::from_str(cipher, false)
1090 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
1091 key_file: PathBuf::from(key),
1092 disk: Box::new(kind.parse()?),
1093 }
1094 }
1095 kind => {
1096 let (path, create_with_len) = parse_path_and_len(s)?;
1101 if path.has_root() {
1102 DiskCliKind::File {
1103 path,
1104 create_with_len,
1105 }
1106 } else {
1107 anyhow::bail!("invalid disk kind {kind}");
1108 }
1109 }
1110 },
1111 };
1112 Ok(disk)
1113 }
1114}
1115
1116#[derive(Clone)]
1117pub struct VmgsCli {
1118 pub kind: DiskCliKind,
1119 pub provision: ProvisionVmgs,
1120}
1121
1122#[derive(Copy, Clone)]
1123pub enum ProvisionVmgs {
1124 OnEmpty,
1125 OnFailure,
1126 True,
1127}
1128
1129impl FromStr for VmgsCli {
1130 type Err = anyhow::Error;
1131
1132 fn from_str(s: &str) -> anyhow::Result<Self> {
1133 let (kind, opt) = s
1134 .split_once(',')
1135 .map(|(k, o)| (k, Some(o)))
1136 .unwrap_or((s, None));
1137 let kind = kind.parse()?;
1138
1139 let provision = match opt {
1140 None => ProvisionVmgs::OnEmpty,
1141 Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
1142 Some("fmt") => ProvisionVmgs::True,
1143 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
1144 };
1145
1146 Ok(VmgsCli { kind, provision })
1147 }
1148}
1149
1150#[derive(Clone)]
1152pub struct DiskCli {
1153 pub vtl: DeviceVtl,
1154 pub kind: DiskCliKind,
1155 pub read_only: bool,
1156 pub is_dvd: bool,
1157 pub underhill: Option<UnderhillDiskSource>,
1158 pub pcie_port: Option<String>,
1159}
1160
1161#[derive(Copy, Clone)]
1162pub enum UnderhillDiskSource {
1163 Scsi,
1164 Nvme,
1165}
1166
1167impl FromStr for DiskCli {
1168 type Err = anyhow::Error;
1169
1170 fn from_str(s: &str) -> anyhow::Result<Self> {
1171 let mut opts = s.split(',');
1172 let kind = opts.next().unwrap().parse()?;
1173
1174 let mut read_only = false;
1175 let mut is_dvd = false;
1176 let mut underhill = None;
1177 let mut vtl = DeviceVtl::Vtl0;
1178 let mut pcie_port = None;
1179 for opt in opts {
1180 let mut s = opt.split('=');
1181 let opt = s.next().unwrap();
1182 match opt {
1183 "ro" => read_only = true,
1184 "dvd" => {
1185 is_dvd = true;
1186 read_only = true;
1187 }
1188 "vtl2" => {
1189 vtl = DeviceVtl::Vtl2;
1190 }
1191 "uh" => underhill = Some(UnderhillDiskSource::Scsi),
1192 "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
1193 "pcie_port" => {
1194 let port = s.next();
1195 if port.is_none_or(|p| p.is_empty()) {
1196 anyhow::bail!("`pcie_port` requires a port name");
1197 }
1198 pcie_port = Some(String::from(port.unwrap()));
1199 }
1200 opt => anyhow::bail!("unknown option: '{opt}'"),
1201 }
1202 }
1203
1204 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
1205 anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
1206 }
1207
1208 if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
1209 anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
1210 }
1211
1212 Ok(DiskCli {
1213 vtl,
1214 kind,
1215 read_only,
1216 is_dvd,
1217 underhill,
1218 pcie_port,
1219 })
1220 }
1221}
1222
1223#[derive(Clone)]
1225pub struct IdeDiskCli {
1226 pub kind: DiskCliKind,
1227 pub read_only: bool,
1228 pub channel: Option<u8>,
1229 pub device: Option<u8>,
1230 pub is_dvd: bool,
1231}
1232
1233impl FromStr for IdeDiskCli {
1234 type Err = anyhow::Error;
1235
1236 fn from_str(s: &str) -> anyhow::Result<Self> {
1237 let mut opts = s.split(',');
1238 let kind = opts.next().unwrap().parse()?;
1239
1240 let mut read_only = false;
1241 let mut channel = None;
1242 let mut device = None;
1243 let mut is_dvd = false;
1244 for opt in opts {
1245 let mut s = opt.split('=');
1246 let opt = s.next().unwrap();
1247 match opt {
1248 "ro" => read_only = true,
1249 "p" => channel = Some(0),
1250 "s" => channel = Some(1),
1251 "0" => device = Some(0),
1252 "1" => device = Some(1),
1253 "dvd" => {
1254 is_dvd = true;
1255 read_only = true;
1256 }
1257 _ => anyhow::bail!("unknown option: '{opt}'"),
1258 }
1259 }
1260
1261 Ok(IdeDiskCli {
1262 kind,
1263 read_only,
1264 channel,
1265 device,
1266 is_dvd,
1267 })
1268 }
1269}
1270
1271#[derive(Clone, Debug, PartialEq)]
1273pub struct FloppyDiskCli {
1274 pub kind: DiskCliKind,
1275 pub read_only: bool,
1276}
1277
1278impl FromStr for FloppyDiskCli {
1279 type Err = anyhow::Error;
1280
1281 fn from_str(s: &str) -> anyhow::Result<Self> {
1282 if s.is_empty() {
1283 anyhow::bail!("empty disk spec");
1284 }
1285 let mut opts = s.split(',');
1286 let kind = opts.next().unwrap().parse()?;
1287
1288 let mut read_only = false;
1289 for opt in opts {
1290 let mut s = opt.split('=');
1291 let opt = s.next().unwrap();
1292 match opt {
1293 "ro" => read_only = true,
1294 _ => anyhow::bail!("unknown option: '{opt}'"),
1295 }
1296 }
1297
1298 Ok(FloppyDiskCli { kind, read_only })
1299 }
1300}
1301
1302#[derive(Clone)]
1303pub struct DebugconSerialConfigCli {
1304 pub port: u16,
1305 pub serial: SerialConfigCli,
1306}
1307
1308impl FromStr for DebugconSerialConfigCli {
1309 type Err = String;
1310
1311 fn from_str(s: &str) -> Result<Self, Self::Err> {
1312 let Some((port, serial)) = s.split_once(',') else {
1313 return Err("invalid format (missing comma between port and serial)".into());
1314 };
1315
1316 let port: u16 = parse_number(port)
1317 .map_err(|_| "could not parse port".to_owned())?
1318 .try_into()
1319 .map_err(|_| "port must be 16-bit")?;
1320 let serial: SerialConfigCli = serial.parse()?;
1321
1322 Ok(Self { port, serial })
1323 }
1324}
1325
1326#[derive(Clone, Debug, PartialEq)]
1328pub enum SerialConfigCli {
1329 None,
1330 Console,
1331 NewConsole(Option<PathBuf>, Option<String>),
1332 Stderr,
1333 Pipe(PathBuf),
1334 Tcp(SocketAddr),
1335 File(PathBuf),
1336}
1337
1338impl FromStr for SerialConfigCli {
1339 type Err = String;
1340
1341 fn from_str(s: &str) -> Result<Self, Self::Err> {
1342 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1343
1344 let first_key = match keyvalues.first() {
1345 Some(first_pair) => first_pair.0.as_str(),
1346 None => Err("invalid serial configuration: no values supplied")?,
1347 };
1348 let first_value = keyvalues.first().unwrap().1.as_ref();
1349
1350 let ret = match first_key {
1351 "none" => SerialConfigCli::None,
1352 "console" => SerialConfigCli::Console,
1353 "stderr" => SerialConfigCli::Stderr,
1354 "file" => match first_value {
1355 Some(path) => SerialConfigCli::File(path.into()),
1356 None => Err("invalid serial configuration: file requires a value")?,
1357 },
1358 "term" => {
1359 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1361 let window_name = match window_name {
1362 Some((_, Some(name))) => Some(name.clone()),
1363 _ => None,
1364 };
1365
1366 SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
1367 }
1368 "listen" => match first_value {
1369 Some(path) => {
1370 if let Some(tcp) = path.strip_prefix("tcp:") {
1371 let addr = tcp
1372 .parse()
1373 .map_err(|err| format!("invalid tcp address: {err}"))?;
1374 SerialConfigCli::Tcp(addr)
1375 } else {
1376 SerialConfigCli::Pipe(path.into())
1377 }
1378 }
1379 None => Err(
1380 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1381 )?,
1382 },
1383 _ => {
1384 return Err(format!(
1385 "invalid serial configuration: '{}' is not a known option",
1386 first_key
1387 ));
1388 }
1389 };
1390
1391 Ok(ret)
1392 }
1393}
1394
1395impl SerialConfigCli {
1396 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1399 let mut ret = Vec::new();
1400
1401 for item in s.split(',') {
1403 let mut eqsplit = item.split('=');
1406 let key = eqsplit.next();
1407 let value = eqsplit.next();
1408
1409 if let Some(key) = key {
1410 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1411 } else {
1412 return Err("invalid key=value pair in serial config".into());
1414 }
1415 }
1416 Ok(ret)
1417 }
1418}
1419
1420#[derive(Clone, Debug, PartialEq)]
1421pub enum EndpointConfigCli {
1422 None,
1423 Consomme { cidr: Option<String> },
1424 Dio { id: Option<String> },
1425 Tap { name: String },
1426}
1427
1428impl FromStr for EndpointConfigCli {
1429 type Err = String;
1430
1431 fn from_str(s: &str) -> Result<Self, Self::Err> {
1432 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1433 ["none"] => EndpointConfigCli::None,
1434 ["consomme", s @ ..] => EndpointConfigCli::Consomme {
1435 cidr: s.first().map(|&s| s.to_owned()),
1436 },
1437 ["dio", s @ ..] => EndpointConfigCli::Dio {
1438 id: s.first().map(|s| (*s).to_owned()),
1439 },
1440 ["tap", name] => EndpointConfigCli::Tap {
1441 name: (*name).to_owned(),
1442 },
1443 _ => return Err("invalid network backend".into()),
1444 };
1445
1446 Ok(ret)
1447 }
1448}
1449
1450#[derive(Clone, Debug, PartialEq)]
1451pub struct NicConfigCli {
1452 pub vtl: DeviceVtl,
1453 pub endpoint: EndpointConfigCli,
1454 pub max_queues: Option<u16>,
1455 pub underhill: bool,
1456 pub pcie_port: Option<String>,
1457}
1458
1459impl FromStr for NicConfigCli {
1460 type Err = String;
1461
1462 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1463 let mut vtl = DeviceVtl::Vtl0;
1464 let mut max_queues = None;
1465 let mut underhill = false;
1466 let mut pcie_port = None;
1467 while let Some((opt, rest)) = s.split_once(':') {
1468 if let Some((opt, val)) = opt.split_once('=') {
1469 match opt {
1470 "queues" => {
1471 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1472 }
1473 "pcie_port" => {
1474 if val.is_empty() {
1475 return Err("`pcie_port=` requires port name argument".into());
1476 }
1477 pcie_port = Some(val.to_string());
1478 }
1479 _ => break,
1480 }
1481 } else {
1482 match opt {
1483 "vtl2" => {
1484 vtl = DeviceVtl::Vtl2;
1485 }
1486 "uh" => underhill = true,
1487 _ => break,
1488 }
1489 }
1490 s = rest;
1491 }
1492
1493 if underhill && vtl != DeviceVtl::Vtl0 {
1494 return Err("`uh` is incompatible with `vtl2`".into());
1495 }
1496
1497 if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
1498 return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
1499 }
1500
1501 let endpoint = s.parse()?;
1502 Ok(NicConfigCli {
1503 vtl,
1504 endpoint,
1505 max_queues,
1506 underhill,
1507 pcie_port,
1508 })
1509 }
1510}
1511
1512#[derive(Debug, Error)]
1513#[error("unknown VTL2 relocation type: {0}")]
1514pub struct UnknownVtl2RelocationType(String);
1515
1516fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1517 match s {
1518 "disable" => Ok(Vtl2BaseAddressType::File),
1519 s if s.starts_with("auto=") => {
1520 let s = s.strip_prefix("auto=").unwrap_or_default();
1521 let size = if s == "filesize" {
1522 None
1523 } else {
1524 let size = parse_memory(s).map_err(|e| {
1525 UnknownVtl2RelocationType(format!(
1526 "unable to parse memory size from {} for 'auto=' type, {e}",
1527 e
1528 ))
1529 })?;
1530 Some(size)
1531 };
1532 Ok(Vtl2BaseAddressType::MemoryLayout { size })
1533 }
1534 s if s.starts_with("absolute=") => {
1535 let s = s.strip_prefix("absolute=");
1536 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
1537 UnknownVtl2RelocationType(format!(
1538 "unable to parse number from {} for 'absolute=' type",
1539 e
1540 ))
1541 })?;
1542 Ok(Vtl2BaseAddressType::Absolute(addr))
1543 }
1544 s if s.starts_with("vtl2=") => {
1545 let s = s.strip_prefix("vtl2=").unwrap_or_default();
1546 let size = if s == "filesize" {
1547 None
1548 } else {
1549 let size = parse_memory(s).map_err(|e| {
1550 UnknownVtl2RelocationType(format!(
1551 "unable to parse memory size from {} for 'vtl2=' type, {e}",
1552 e
1553 ))
1554 })?;
1555 Some(size)
1556 };
1557 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
1558 }
1559 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
1560 }
1561}
1562
1563#[derive(Debug, Copy, Clone, PartialEq)]
1564pub enum SmtConfigCli {
1565 Auto,
1566 Force,
1567 Off,
1568}
1569
1570#[derive(Debug, Error)]
1571#[error("expected auto, force, or off")]
1572pub struct BadSmtConfig;
1573
1574impl FromStr for SmtConfigCli {
1575 type Err = BadSmtConfig;
1576
1577 fn from_str(s: &str) -> Result<Self, Self::Err> {
1578 let r = match s {
1579 "auto" => Self::Auto,
1580 "force" => Self::Force,
1581 "off" => Self::Off,
1582 _ => return Err(BadSmtConfig),
1583 };
1584 Ok(r)
1585 }
1586}
1587
1588#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
1589fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
1590 let r = match s {
1591 "auto" => X2ApicConfig::Auto,
1592 "supported" => X2ApicConfig::Supported,
1593 "off" => X2ApicConfig::Unsupported,
1594 "on" => X2ApicConfig::Enabled,
1595 _ => return Err("expected auto, supported, off, or on"),
1596 };
1597 Ok(r)
1598}
1599
1600#[derive(Debug, Copy, Clone, ValueEnum)]
1601pub enum Vtl0LateMapPolicyCli {
1602 Off,
1603 Log,
1604 Halt,
1605 Exception,
1606}
1607
1608#[derive(Debug, Copy, Clone, ValueEnum)]
1609pub enum IsolationCli {
1610 Vbs,
1611}
1612
1613#[derive(Debug, Copy, Clone, PartialEq)]
1614pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
1615
1616impl FromStr for PcatBootOrderCli {
1617 type Err = &'static str;
1618
1619 fn from_str(s: &str) -> Result<Self, Self::Err> {
1620 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
1621 let mut order = Vec::new();
1622
1623 for item in s.split(',') {
1624 let device = match item {
1625 "optical" => PcatBootDevice::Optical,
1626 "hdd" => PcatBootDevice::HardDrive,
1627 "net" => PcatBootDevice::Network,
1628 "floppy" => PcatBootDevice::Floppy,
1629 _ => return Err("unknown boot device type"),
1630 };
1631
1632 let default_pos = default_order
1633 .iter()
1634 .position(|x| x == &Some(device))
1635 .ok_or("cannot pass duplicate boot devices")?;
1636
1637 order.push(default_order[default_pos].take().unwrap());
1638 }
1639
1640 order.extend(default_order.into_iter().flatten());
1641 assert_eq!(order.len(), 4);
1642
1643 Ok(Self(order.try_into().unwrap()))
1644 }
1645}
1646
1647#[derive(Copy, Clone, Debug, ValueEnum)]
1648pub enum UefiConsoleModeCli {
1649 Default,
1650 Com1,
1651 Com2,
1652 None,
1653}
1654
1655#[derive(Copy, Clone, Debug, Default, ValueEnum)]
1656pub enum EfiDiagnosticsLogLevelCli {
1657 #[default]
1658 Default,
1659 Info,
1660 Full,
1661}
1662
1663#[derive(Clone, Debug, PartialEq)]
1664pub struct PcieRootComplexCli {
1665 pub name: String,
1666 pub segment: u16,
1667 pub start_bus: u8,
1668 pub end_bus: u8,
1669 pub low_mmio: u32,
1670 pub high_mmio: u64,
1671}
1672
1673impl FromStr for PcieRootComplexCli {
1674 type Err = anyhow::Error;
1675
1676 fn from_str(s: &str) -> Result<Self, Self::Err> {
1677 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(',');
1681 let name = opts.next().context("expected root complex name")?;
1682 if name.is_empty() {
1683 anyhow::bail!("must provide a root complex name");
1684 }
1685
1686 let mut segment = 0;
1687 let mut start_bus = 0;
1688 let mut end_bus = 255;
1689 let mut low_mmio = DEFAULT_PCIE_CRS_LOW_SIZE;
1690 let mut high_mmio = DEFAULT_PCIE_CRS_HIGH_SIZE;
1691 for opt in opts {
1692 let mut s = opt.split('=');
1693 let opt = s.next().context("expected option")?;
1694 match opt {
1695 "segment" => {
1696 let seg_str = s.next().context("expected segment number")?;
1697 segment = u16::from_str(seg_str).context("failed to parse segment number")?;
1698 }
1699 "start_bus" => {
1700 let bus_str = s.next().context("expected start bus number")?;
1701 start_bus =
1702 u8::from_str(bus_str).context("failed to parse start bus number")?;
1703 }
1704 "end_bus" => {
1705 let bus_str = s.next().context("expected end bus number")?;
1706 end_bus = u8::from_str(bus_str).context("failed to parse end bus number")?;
1707 }
1708 "low_mmio" => {
1709 let low_mmio_str = s.next().context("expected low MMIO size")?;
1710 low_mmio = parse_memory(low_mmio_str)
1711 .context("failed to parse low MMIO size")?
1712 .try_into()?;
1713 }
1714 "high_mmio" => {
1715 let high_mmio_str = s.next().context("expected high MMIO size")?;
1716 high_mmio =
1717 parse_memory(high_mmio_str).context("failed to parse high MMIO size")?;
1718 }
1719 opt => anyhow::bail!("unknown option: '{opt}'"),
1720 }
1721 }
1722
1723 if start_bus >= end_bus {
1724 anyhow::bail!("start_bus must be less than or equal to end_bus");
1725 }
1726
1727 Ok(PcieRootComplexCli {
1728 name: name.to_string(),
1729 segment,
1730 start_bus,
1731 end_bus,
1732 low_mmio,
1733 high_mmio,
1734 })
1735 }
1736}
1737
1738#[derive(Clone, Debug, PartialEq)]
1739pub struct PcieRootPortCli {
1740 pub root_complex_name: String,
1741 pub name: String,
1742 pub hotplug: bool,
1743}
1744
1745impl FromStr for PcieRootPortCli {
1746 type Err = anyhow::Error;
1747
1748 fn from_str(s: &str) -> Result<Self, Self::Err> {
1749 let mut opts = s.split(',');
1750 let names = opts.next().context("expected root port identifiers")?;
1751 if names.is_empty() {
1752 anyhow::bail!("must provide root port identifiers");
1753 }
1754
1755 let mut s = names.split(':');
1756 let rc_name = s.next().context("expected name of parent root complex")?;
1757 let rp_name = s.next().context("expected root port name")?;
1758
1759 if let Some(extra) = s.next() {
1760 anyhow::bail!("unexpected token: '{extra}'")
1761 }
1762
1763 let mut hotplug = false;
1764
1765 for opt in opts {
1767 match opt {
1768 "hotplug" => hotplug = true,
1769 _ => anyhow::bail!("unexpected option: '{opt}'"),
1770 }
1771 }
1772
1773 Ok(PcieRootPortCli {
1774 root_complex_name: rc_name.to_string(),
1775 name: rp_name.to_string(),
1776 hotplug,
1777 })
1778 }
1779}
1780
1781#[derive(Clone, Debug, PartialEq)]
1782pub struct GenericPcieSwitchCli {
1783 pub port_name: String,
1784 pub name: String,
1785 pub num_downstream_ports: u8,
1786 pub hotplug: bool,
1787}
1788
1789impl FromStr for GenericPcieSwitchCli {
1790 type Err = anyhow::Error;
1791
1792 fn from_str(s: &str) -> Result<Self, Self::Err> {
1793 let mut opts = s.split(',');
1794 let names = opts.next().context("expected switch identifiers")?;
1795 if names.is_empty() {
1796 anyhow::bail!("must provide switch identifiers");
1797 }
1798
1799 let mut s = names.split(':');
1800 let port_name = s.next().context("expected name of parent port")?;
1801 let switch_name = s.next().context("expected switch name")?;
1802
1803 if let Some(extra) = s.next() {
1804 anyhow::bail!("unexpected token: '{extra}'")
1805 }
1806
1807 let mut num_downstream_ports = 4u8; let mut hotplug = false;
1809
1810 for opt in opts {
1811 let mut kv = opt.split('=');
1812 let key = kv.next().context("expected option name")?;
1813
1814 match key {
1815 "num_downstream_ports" => {
1816 let value = kv.next().context("expected option value")?;
1817 if let Some(extra) = kv.next() {
1818 anyhow::bail!("unexpected token: '{extra}'")
1819 }
1820 num_downstream_ports = value.parse().context("invalid num_downstream_ports")?;
1821 }
1822 "hotplug" => {
1823 if kv.next().is_some() {
1824 anyhow::bail!("hotplug option does not take a value")
1825 }
1826 hotplug = true;
1827 }
1828 _ => anyhow::bail!("unknown option: '{key}'"),
1829 }
1830 }
1831
1832 Ok(GenericPcieSwitchCli {
1833 port_name: port_name.to_string(),
1834 name: switch_name.to_string(),
1835 num_downstream_ports,
1836 hotplug,
1837 })
1838 }
1839}
1840
1841#[derive(Clone, Debug, PartialEq)]
1843pub struct PcieRemoteCli {
1844 pub port_name: String,
1846 pub socket_addr: Option<String>,
1848 pub hu: u16,
1850 pub controller: u16,
1852}
1853
1854impl FromStr for PcieRemoteCli {
1855 type Err = anyhow::Error;
1856
1857 fn from_str(s: &str) -> Result<Self, Self::Err> {
1858 let mut opts = s.split(',');
1859 let port_name = opts.next().context("expected port name")?;
1860 if port_name.is_empty() {
1861 anyhow::bail!("must provide a port name");
1862 }
1863
1864 let mut socket_addr = None;
1865 let mut hu = 0u16;
1866 let mut controller = 0u16;
1867
1868 for opt in opts {
1869 let mut kv = opt.split('=');
1870 let key = kv.next().context("expected option name")?;
1871 let value = kv.next();
1872
1873 match key {
1874 "socket" => {
1875 let addr = value.context("socket requires an address")?;
1876 if let Some(extra) = kv.next() {
1877 anyhow::bail!("unexpected token: '{extra}'")
1878 }
1879 if addr.is_empty() {
1880 anyhow::bail!("socket address cannot be empty");
1881 }
1882 socket_addr = Some(addr.to_string());
1883 }
1884 "hu" => {
1885 let val = value.context("hu requires a value")?;
1886 if let Some(extra) = kv.next() {
1887 anyhow::bail!("unexpected token: '{extra}'")
1888 }
1889 hu = val.parse().context("failed to parse hu")?;
1890 }
1891 "controller" => {
1892 let val = value.context("controller requires a value")?;
1893 if let Some(extra) = kv.next() {
1894 anyhow::bail!("unexpected token: '{extra}'")
1895 }
1896 controller = val.parse().context("failed to parse controller")?;
1897 }
1898 _ => anyhow::bail!("unknown option: '{key}'"),
1899 }
1900 }
1901
1902 Ok(PcieRemoteCli {
1903 port_name: port_name.to_string(),
1904 socket_addr,
1905 hu,
1906 controller,
1907 })
1908 }
1909}
1910
1911fn default_value_from_arch_env(name: &str) -> OsString {
1919 let prefix = if cfg!(guest_arch = "x86_64") {
1920 "X86_64"
1921 } else if cfg!(guest_arch = "aarch64") {
1922 "AARCH64"
1923 } else {
1924 return Default::default();
1925 };
1926 let prefixed = format!("{}_{}", prefix, name);
1927 std::env::var_os(name)
1928 .or_else(|| std::env::var_os(prefixed))
1929 .unwrap_or_default()
1930}
1931
1932#[derive(Clone)]
1934pub struct OptionalPathBuf(pub Option<PathBuf>);
1935
1936impl From<&std::ffi::OsStr> for OptionalPathBuf {
1937 fn from(s: &std::ffi::OsStr) -> Self {
1938 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
1939 }
1940}
1941
1942#[cfg(test)]
1943#[expect(unsafe_code)]
1945mod tests {
1946 use super::*;
1947
1948 fn with_env_var<F, R>(name: &str, value: &str, f: F) -> R
1949 where
1950 F: FnOnce() -> R,
1951 {
1952 unsafe {
1955 std::env::set_var(name, value);
1956 }
1957 let result = f();
1958 unsafe {
1961 std::env::remove_var(name);
1962 }
1963 result
1964 }
1965
1966 #[test]
1967 fn test_parse_file_disk_with_create() {
1968 let s = "file:test.vhd;create=1G";
1969 let disk = DiskCliKind::from_str(s).unwrap();
1970
1971 match disk {
1972 DiskCliKind::File {
1973 path,
1974 create_with_len,
1975 } => {
1976 assert_eq!(path, PathBuf::from("test.vhd"));
1977 assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); }
1979 _ => panic!("Expected File variant"),
1980 }
1981 }
1982
1983 #[test]
1984 fn test_parse_direct_file_with_create() {
1985 let s = "test.vhd;create=1G";
1986 let disk = DiskCliKind::from_str(s).unwrap();
1987
1988 match disk {
1989 DiskCliKind::File {
1990 path,
1991 create_with_len,
1992 } => {
1993 assert_eq!(path, PathBuf::from("test.vhd"));
1994 assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); }
1996 _ => panic!("Expected File variant"),
1997 }
1998 }
1999
2000 #[test]
2001 fn test_parse_memory_disk() {
2002 let s = "mem:1G";
2003 let disk = DiskCliKind::from_str(s).unwrap();
2004 match disk {
2005 DiskCliKind::Memory(size) => {
2006 assert_eq!(size, 1024 * 1024 * 1024); }
2008 _ => panic!("Expected Memory variant"),
2009 }
2010 }
2011
2012 #[test]
2013 fn test_parse_pcie_disk() {
2014 assert_eq!(
2015 DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
2016 Some("p0".to_string())
2017 );
2018 assert_eq!(
2019 DiskCli::from_str("file:path.vhdx,pcie_port=p0")
2020 .unwrap()
2021 .pcie_port,
2022 Some("p0".to_string())
2023 );
2024 assert_eq!(
2025 DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
2026 .unwrap()
2027 .pcie_port,
2028 Some("p0".to_string())
2029 );
2030
2031 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
2033
2034 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
2036 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
2037 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
2038 }
2039
2040 #[test]
2041 fn test_parse_memory_diff_disk() {
2042 let s = "memdiff:file:base.img";
2043 let disk = DiskCliKind::from_str(s).unwrap();
2044 match disk {
2045 DiskCliKind::MemoryDiff(inner) => match *inner {
2046 DiskCliKind::File {
2047 path,
2048 create_with_len,
2049 } => {
2050 assert_eq!(path, PathBuf::from("base.img"));
2051 assert_eq!(create_with_len, None);
2052 }
2053 _ => panic!("Expected File variant inside MemoryDiff"),
2054 },
2055 _ => panic!("Expected MemoryDiff variant"),
2056 }
2057 }
2058
2059 #[test]
2060 fn test_parse_sqlite_disk() {
2061 let s = "sql:db.sqlite;create=2G";
2062 let disk = DiskCliKind::from_str(s).unwrap();
2063 match disk {
2064 DiskCliKind::Sqlite {
2065 path,
2066 create_with_len,
2067 } => {
2068 assert_eq!(path, PathBuf::from("db.sqlite"));
2069 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
2070 }
2071 _ => panic!("Expected Sqlite variant"),
2072 }
2073
2074 let s = "sql:db.sqlite";
2076 let disk = DiskCliKind::from_str(s).unwrap();
2077 match disk {
2078 DiskCliKind::Sqlite {
2079 path,
2080 create_with_len,
2081 } => {
2082 assert_eq!(path, PathBuf::from("db.sqlite"));
2083 assert_eq!(create_with_len, None);
2084 }
2085 _ => panic!("Expected Sqlite variant"),
2086 }
2087 }
2088
2089 #[test]
2090 fn test_parse_sqlite_diff_disk() {
2091 let s = "sqldiff:diff.sqlite;create:file:base.img";
2093 let disk = DiskCliKind::from_str(s).unwrap();
2094 match disk {
2095 DiskCliKind::SqliteDiff { path, create, disk } => {
2096 assert_eq!(path, PathBuf::from("diff.sqlite"));
2097 assert!(create);
2098 match *disk {
2099 DiskCliKind::File {
2100 path,
2101 create_with_len,
2102 } => {
2103 assert_eq!(path, PathBuf::from("base.img"));
2104 assert_eq!(create_with_len, None);
2105 }
2106 _ => panic!("Expected File variant inside SqliteDiff"),
2107 }
2108 }
2109 _ => panic!("Expected SqliteDiff variant"),
2110 }
2111
2112 let s = "sqldiff:diff.sqlite:file:base.img";
2114 let disk = DiskCliKind::from_str(s).unwrap();
2115 match disk {
2116 DiskCliKind::SqliteDiff { path, create, disk } => {
2117 assert_eq!(path, PathBuf::from("diff.sqlite"));
2118 assert!(!create);
2119 match *disk {
2120 DiskCliKind::File {
2121 path,
2122 create_with_len,
2123 } => {
2124 assert_eq!(path, PathBuf::from("base.img"));
2125 assert_eq!(create_with_len, None);
2126 }
2127 _ => panic!("Expected File variant inside SqliteDiff"),
2128 }
2129 }
2130 _ => panic!("Expected SqliteDiff variant"),
2131 }
2132 }
2133
2134 #[test]
2135 fn test_parse_autocache_sqlite_disk() {
2136 let disk = with_env_var("OPENVMM_AUTO_CACHE_PATH", "/tmp/cache", || {
2138 DiskCliKind::from_str("autocache::file:disk.vhd").unwrap()
2139 });
2140 assert!(matches!(
2141 disk,
2142 DiskCliKind::AutoCacheSqlite {
2143 cache_path,
2144 key,
2145 disk: _disk,
2146 } if cache_path == "/tmp/cache" && key.is_none()
2147 ));
2148
2149 assert!(DiskCliKind::from_str("autocache::file:disk.vhd").is_err());
2151 }
2152
2153 #[test]
2154 fn test_parse_disk_errors() {
2155 assert!(DiskCliKind::from_str("invalid:").is_err());
2156 assert!(DiskCliKind::from_str("memory:extra").is_err());
2157
2158 assert!(DiskCliKind::from_str("sqlite:").is_err());
2160 }
2161
2162 #[test]
2163 fn test_parse_errors() {
2164 assert!(DiskCliKind::from_str("mem:invalid").is_err());
2166
2167 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
2169
2170 unsafe {
2174 std::env::remove_var("OPENVMM_AUTO_CACHE_PATH");
2175 }
2176 assert!(DiskCliKind::from_str("autocache:key:file:disk.vhd").is_err());
2177
2178 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
2180
2181 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
2183
2184 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
2186
2187 assert!(DiskCliKind::from_str("invalid:path").is_err());
2189
2190 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
2192 }
2193
2194 #[test]
2195 fn test_fs_args_from_str() {
2196 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
2197 assert_eq!(args.tag, "tag1");
2198 assert_eq!(args.path, "/path/to/fs");
2199
2200 assert!(FsArgs::from_str("tag1").is_err());
2202 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
2203 }
2204
2205 #[test]
2206 fn test_fs_args_with_options_from_str() {
2207 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
2208 assert_eq!(args.tag, "tag1");
2209 assert_eq!(args.path, "/path/to/fs");
2210 assert_eq!(args.options, "opt1;opt2");
2211
2212 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
2214 assert_eq!(args.tag, "tag1");
2215 assert_eq!(args.path, "/path/to/fs");
2216 assert_eq!(args.options, "");
2217
2218 assert!(FsArgsWithOptions::from_str("tag1").is_err());
2220 }
2221
2222 #[test]
2223 fn test_serial_config_from_str() {
2224 assert_eq!(
2225 SerialConfigCli::from_str("none").unwrap(),
2226 SerialConfigCli::None
2227 );
2228 assert_eq!(
2229 SerialConfigCli::from_str("console").unwrap(),
2230 SerialConfigCli::Console
2231 );
2232 assert_eq!(
2233 SerialConfigCli::from_str("stderr").unwrap(),
2234 SerialConfigCli::Stderr
2235 );
2236
2237 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
2239 if let SerialConfigCli::File(path) = file_config {
2240 assert_eq!(path.to_str().unwrap(), "/path/to/file");
2241 } else {
2242 panic!("Expected File variant");
2243 }
2244
2245 match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
2247 SerialConfigCli::NewConsole(None, Some(name)) => {
2248 assert_eq!(name, "MyTerm");
2249 }
2250 _ => panic!("Expected NewConsole variant with name"),
2251 }
2252
2253 match SerialConfigCli::from_str("term").unwrap() {
2255 SerialConfigCli::NewConsole(None, None) => (),
2256 _ => panic!("Expected NewConsole variant without name"),
2257 }
2258
2259 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
2261 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
2262 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
2263 assert_eq!(name, "MyTerm");
2264 }
2265 _ => panic!("Expected NewConsole variant with name"),
2266 }
2267
2268 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
2270 SerialConfigCli::NewConsole(Some(path), None) => {
2271 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
2272 }
2273 _ => panic!("Expected NewConsole variant without name"),
2274 }
2275
2276 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
2278 SerialConfigCli::Tcp(addr) => {
2279 assert_eq!(addr.to_string(), "127.0.0.1:1234");
2280 }
2281 _ => panic!("Expected Tcp variant"),
2282 }
2283
2284 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
2286 SerialConfigCli::Pipe(path) => {
2287 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
2288 }
2289 _ => panic!("Expected Pipe variant"),
2290 }
2291
2292 assert!(SerialConfigCli::from_str("").is_err());
2294 assert!(SerialConfigCli::from_str("unknown").is_err());
2295 assert!(SerialConfigCli::from_str("file").is_err());
2296 assert!(SerialConfigCli::from_str("listen").is_err());
2297 }
2298
2299 #[test]
2300 fn test_endpoint_config_from_str() {
2301 assert!(matches!(
2303 EndpointConfigCli::from_str("none").unwrap(),
2304 EndpointConfigCli::None
2305 ));
2306
2307 match EndpointConfigCli::from_str("consomme").unwrap() {
2309 EndpointConfigCli::Consomme { cidr: None } => (),
2310 _ => panic!("Expected Consomme variant without cidr"),
2311 }
2312
2313 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
2315 EndpointConfigCli::Consomme { cidr: Some(cidr) } => {
2316 assert_eq!(cidr, "192.168.0.0/24");
2317 }
2318 _ => panic!("Expected Consomme variant with cidr"),
2319 }
2320
2321 match EndpointConfigCli::from_str("dio").unwrap() {
2323 EndpointConfigCli::Dio { id: None } => (),
2324 _ => panic!("Expected Dio variant without id"),
2325 }
2326
2327 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
2329 EndpointConfigCli::Dio { id: Some(id) } => {
2330 assert_eq!(id, "test_id");
2331 }
2332 _ => panic!("Expected Dio variant with id"),
2333 }
2334
2335 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
2337 EndpointConfigCli::Tap { name } => {
2338 assert_eq!(name, "tap0");
2339 }
2340 _ => panic!("Expected Tap variant"),
2341 }
2342
2343 assert!(EndpointConfigCli::from_str("invalid").is_err());
2345 }
2346
2347 #[test]
2348 fn test_nic_config_from_str() {
2349 use openvmm_defs::config::DeviceVtl;
2350
2351 let config = NicConfigCli::from_str("none").unwrap();
2353 assert_eq!(config.vtl, DeviceVtl::Vtl0);
2354 assert!(config.max_queues.is_none());
2355 assert!(!config.underhill);
2356 assert!(config.pcie_port.is_none());
2357 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2358
2359 let config = NicConfigCli::from_str("vtl2:none").unwrap();
2361 assert_eq!(config.vtl, DeviceVtl::Vtl2);
2362 assert!(config.pcie_port.is_none());
2363 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2364
2365 let config = NicConfigCli::from_str("queues=4:none").unwrap();
2367 assert_eq!(config.max_queues, Some(4));
2368 assert!(config.pcie_port.is_none());
2369 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2370
2371 let config = NicConfigCli::from_str("uh:none").unwrap();
2373 assert!(config.underhill);
2374 assert!(config.pcie_port.is_none());
2375 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2376
2377 let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
2379 assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
2380 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2381
2382 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
2384 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
2386 assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
2387 assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
2388 assert!(NicConfigCli::from_str("pcie_port:none").is_err());
2389 }
2390
2391 #[test]
2392 fn test_parse_pcie_port_prefix() {
2393 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
2395 assert_eq!(port.unwrap(), "rp0");
2396 assert_eq!(rest, "tag,path");
2397
2398 let (port, rest) = parse_pcie_port_prefix("tag,path");
2400 assert!(port.is_none());
2401 assert_eq!(rest, "tag,path");
2402
2403 let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
2405 assert!(port.is_none());
2406 assert_eq!(rest, "pcie_port=:tag,path");
2407
2408 let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
2410 assert!(port.is_none());
2411 assert_eq!(rest, "pcie_port=rp0");
2412 }
2413
2414 #[test]
2415 fn test_fs_args_pcie_port() {
2416 let args = FsArgs::from_str("myfs,/path").unwrap();
2418 assert_eq!(args.tag, "myfs");
2419 assert_eq!(args.path, "/path");
2420 assert!(args.pcie_port.is_none());
2421
2422 let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
2424 assert_eq!(args.pcie_port.unwrap(), "rp0");
2425 assert_eq!(args.tag, "myfs");
2426 assert_eq!(args.path, "/path");
2427
2428 assert!(FsArgs::from_str("myfs").is_err());
2430 assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
2431 }
2432
2433 #[test]
2434 fn test_fs_args_with_options_pcie_port() {
2435 let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
2437 assert_eq!(args.tag, "myfs");
2438 assert_eq!(args.path, "/path");
2439 assert_eq!(args.options, "uid=1000");
2440 assert!(args.pcie_port.is_none());
2441
2442 let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
2444 assert_eq!(args.pcie_port.unwrap(), "rp0");
2445 assert_eq!(args.tag, "myfs");
2446 assert_eq!(args.path, "/path");
2447 assert_eq!(args.options, "uid=1000");
2448
2449 assert!(FsArgsWithOptions::from_str("myfs").is_err());
2451 }
2452
2453 #[test]
2454 fn test_virtio_pmem_args_pcie_port() {
2455 let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
2457 assert_eq!(args.path, "/path/to/file");
2458 assert!(args.pcie_port.is_none());
2459
2460 let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
2462 assert_eq!(args.pcie_port.unwrap(), "rp0");
2463 assert_eq!(args.path, "/path/to/file");
2464
2465 assert!(VirtioPmemArgs::from_str("").is_err());
2467 assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
2468 }
2469
2470 #[test]
2471 fn test_smt_config_from_str() {
2472 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
2473 assert_eq!(
2474 SmtConfigCli::from_str("force").unwrap(),
2475 SmtConfigCli::Force
2476 );
2477 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
2478
2479 assert!(SmtConfigCli::from_str("invalid").is_err());
2481 assert!(SmtConfigCli::from_str("").is_err());
2482 }
2483
2484 #[test]
2485 fn test_pcat_boot_order_from_str() {
2486 let order = PcatBootOrderCli::from_str("optical").unwrap();
2488 assert_eq!(order.0[0], PcatBootDevice::Optical);
2489
2490 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
2492 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
2493 assert_eq!(order.0[1], PcatBootDevice::Network);
2494
2495 assert!(PcatBootOrderCli::from_str("invalid").is_err());
2497 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
2499
2500 #[test]
2501 fn test_floppy_disk_from_str() {
2502 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
2504 assert!(!disk.read_only);
2505 match disk.kind {
2506 DiskCliKind::File {
2507 path,
2508 create_with_len,
2509 } => {
2510 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
2511 assert_eq!(create_with_len, None);
2512 }
2513 _ => panic!("Expected File variant"),
2514 }
2515
2516 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
2518 assert!(disk.read_only);
2519
2520 assert!(FloppyDiskCli::from_str("").is_err());
2522 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
2523 }
2524
2525 #[test]
2526 fn test_pcie_root_complex_from_str() {
2527 const ONE_MB: u64 = 1024 * 1024;
2528 const ONE_GB: u64 = 1024 * ONE_MB;
2529
2530 const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
2531 const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
2532
2533 assert_eq!(
2534 PcieRootComplexCli::from_str("rc0").unwrap(),
2535 PcieRootComplexCli {
2536 name: "rc0".to_string(),
2537 segment: 0,
2538 start_bus: 0,
2539 end_bus: 255,
2540 low_mmio: DEFAULT_LOW_MMIO,
2541 high_mmio: DEFAULT_HIGH_MMIO,
2542 }
2543 );
2544
2545 assert_eq!(
2546 PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
2547 PcieRootComplexCli {
2548 name: "rc1".to_string(),
2549 segment: 1,
2550 start_bus: 0,
2551 end_bus: 255,
2552 low_mmio: DEFAULT_LOW_MMIO,
2553 high_mmio: DEFAULT_HIGH_MMIO,
2554 }
2555 );
2556
2557 assert_eq!(
2558 PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
2559 PcieRootComplexCli {
2560 name: "rc2".to_string(),
2561 segment: 0,
2562 start_bus: 32,
2563 end_bus: 255,
2564 low_mmio: DEFAULT_LOW_MMIO,
2565 high_mmio: DEFAULT_HIGH_MMIO,
2566 }
2567 );
2568
2569 assert_eq!(
2570 PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
2571 PcieRootComplexCli {
2572 name: "rc3".to_string(),
2573 segment: 0,
2574 start_bus: 0,
2575 end_bus: 31,
2576 low_mmio: DEFAULT_LOW_MMIO,
2577 high_mmio: DEFAULT_HIGH_MMIO,
2578 }
2579 );
2580
2581 assert_eq!(
2582 PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
2583 PcieRootComplexCli {
2584 name: "rc4".to_string(),
2585 segment: 0,
2586 start_bus: 32,
2587 end_bus: 127,
2588 low_mmio: DEFAULT_LOW_MMIO,
2589 high_mmio: 2 * ONE_GB,
2590 }
2591 );
2592
2593 assert_eq!(
2594 PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
2595 PcieRootComplexCli {
2596 name: "rc5".to_string(),
2597 segment: 2,
2598 start_bus: 32,
2599 end_bus: 127,
2600 low_mmio: DEFAULT_LOW_MMIO,
2601 high_mmio: DEFAULT_HIGH_MMIO,
2602 }
2603 );
2604
2605 assert_eq!(
2606 PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
2607 PcieRootComplexCli {
2608 name: "rc6".to_string(),
2609 segment: 0,
2610 start_bus: 0,
2611 end_bus: 255,
2612 low_mmio: ONE_MB as u32,
2613 high_mmio: 64 * ONE_GB,
2614 }
2615 );
2616
2617 assert!(PcieRootComplexCli::from_str("").is_err());
2619 assert!(PcieRootComplexCli::from_str("poorly,").is_err());
2620 assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
2621 assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
2622 assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
2623 assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
2624 assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
2625 assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
2626 assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
2627 assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
2628 assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
2629 }
2630
2631 #[test]
2632 fn test_pcie_root_port_from_str() {
2633 assert_eq!(
2634 PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
2635 PcieRootPortCli {
2636 root_complex_name: "rc0".to_string(),
2637 name: "rc0rp0".to_string(),
2638 hotplug: false,
2639 }
2640 );
2641
2642 assert_eq!(
2643 PcieRootPortCli::from_str("my_rc:port2").unwrap(),
2644 PcieRootPortCli {
2645 root_complex_name: "my_rc".to_string(),
2646 name: "port2".to_string(),
2647 hotplug: false,
2648 }
2649 );
2650
2651 assert_eq!(
2653 PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
2654 PcieRootPortCli {
2655 root_complex_name: "my_rc".to_string(),
2656 name: "port2".to_string(),
2657 hotplug: true,
2658 }
2659 );
2660
2661 assert!(PcieRootPortCli::from_str("").is_err());
2663 assert!(PcieRootPortCli::from_str("rp0").is_err());
2664 assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
2665 assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
2666 assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
2667 }
2668
2669 #[test]
2670 fn test_pcie_switch_from_str() {
2671 assert_eq!(
2672 GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
2673 GenericPcieSwitchCli {
2674 port_name: "rp0".to_string(),
2675 name: "switch0".to_string(),
2676 num_downstream_ports: 4,
2677 hotplug: false,
2678 }
2679 );
2680
2681 assert_eq!(
2682 GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
2683 GenericPcieSwitchCli {
2684 port_name: "port1".to_string(),
2685 name: "my_switch".to_string(),
2686 num_downstream_ports: 4,
2687 hotplug: false,
2688 }
2689 );
2690
2691 assert_eq!(
2692 GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
2693 GenericPcieSwitchCli {
2694 port_name: "rp2".to_string(),
2695 name: "sw".to_string(),
2696 num_downstream_ports: 8,
2697 hotplug: false,
2698 }
2699 );
2700
2701 assert_eq!(
2703 GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
2704 GenericPcieSwitchCli {
2705 port_name: "switch0-downstream-1".to_string(),
2706 name: "child_switch".to_string(),
2707 num_downstream_ports: 4,
2708 hotplug: false,
2709 }
2710 );
2711
2712 assert_eq!(
2714 GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
2715 GenericPcieSwitchCli {
2716 port_name: "rp0".to_string(),
2717 name: "switch0".to_string(),
2718 num_downstream_ports: 4,
2719 hotplug: true,
2720 }
2721 );
2722
2723 assert_eq!(
2725 GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
2726 GenericPcieSwitchCli {
2727 port_name: "rp0".to_string(),
2728 name: "switch0".to_string(),
2729 num_downstream_ports: 8,
2730 hotplug: true,
2731 }
2732 );
2733
2734 assert!(GenericPcieSwitchCli::from_str("").is_err());
2736 assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
2737 assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
2738 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
2739 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
2740 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
2741 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
2742 }
2743
2744 #[test]
2745 fn test_pcie_remote_from_str() {
2746 assert_eq!(
2748 PcieRemoteCli::from_str("rc0rp0").unwrap(),
2749 PcieRemoteCli {
2750 port_name: "rc0rp0".to_string(),
2751 socket_addr: None,
2752 hu: 0,
2753 controller: 0,
2754 }
2755 );
2756
2757 assert_eq!(
2759 PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
2760 PcieRemoteCli {
2761 port_name: "rc0rp0".to_string(),
2762 socket_addr: Some("localhost:22567".to_string()),
2763 hu: 0,
2764 controller: 0,
2765 }
2766 );
2767
2768 assert_eq!(
2770 PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
2771 PcieRemoteCli {
2772 port_name: "myport".to_string(),
2773 socket_addr: Some("localhost:22568".to_string()),
2774 hu: 1,
2775 controller: 2,
2776 }
2777 );
2778
2779 assert_eq!(
2781 PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
2782 PcieRemoteCli {
2783 port_name: "port0".to_string(),
2784 socket_addr: None,
2785 hu: 5,
2786 controller: 3,
2787 }
2788 );
2789
2790 assert!(PcieRemoteCli::from_str("").is_err());
2792 assert!(PcieRemoteCli::from_str("port,socket=").is_err());
2793 assert!(PcieRemoteCli::from_str("port,hu=").is_err());
2794 assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
2795 assert!(PcieRemoteCli::from_str("port,controller=").is_err());
2796 assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
2797 assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
2798 }
2799}