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::Hypervisor;
27use openvmm_defs::config::PcatBootDevice;
28use openvmm_defs::config::Vtl2BaseAddressType;
29use openvmm_defs::config::X2ApicConfig;
30use std::ffi::OsString;
31use std::net::SocketAddr;
32use std::path::PathBuf;
33use std::str::FromStr;
34use thiserror::Error;
35
36#[derive(Parser)]
41pub struct Options {
42 #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
44 pub processors: u32,
45
46 #[clap(
48 short = 'm',
49 long,
50 value_name = "SIZE",
51 default_value = "1GB",
52 value_parser = parse_memory
53 )]
54 pub memory: u64,
55
56 #[clap(short = 'M', long)]
58 pub shared_memory: bool,
59
60 #[clap(long)]
62 pub prefetch: bool,
63
64 #[clap(short = 'P', long)]
66 pub paused: bool,
67
68 #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
70 pub kernel: OptionalPathBuf,
71
72 #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
74 pub initrd: OptionalPathBuf,
75
76 #[clap(short = 'c', long, value_name = "STRING")]
78 pub cmdline: Vec<String>,
79
80 #[clap(long)]
82 pub hv: bool,
83
84 #[clap(long, requires("hv"))]
88 pub vtl2: bool,
89
90 #[clap(long, requires("hv"))]
93 pub get: bool,
94
95 #[clap(long, conflicts_with("get"))]
98 pub no_get: bool,
99
100 #[clap(long, requires("vtl2"))]
102 pub no_alias_map: bool,
103
104 #[clap(long, requires("vtl2"))]
106 pub isolation: Option<IsolationCli>,
107
108 #[clap(long, value_name = "PATH")]
110 pub vsock_path: Option<String>,
111
112 #[clap(long, value_name = "PATH", requires("vtl2"))]
114 pub vtl2_vsock_path: Option<String>,
115
116 #[clap(long, requires("vtl2"), default_value = "halt")]
118 pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
119
120 #[clap(long)]
122 pub no_enlightenments: bool,
123
124 #[clap(long)]
126 pub user_mode_apic: bool,
127
128 #[clap(long_help = r#"
130e.g: --disk memdiff:file:/path/to/disk.vhd
131
132syntax: <path> | kind:<arg>[,flag,opt=arg,...]
133
134valid disk kinds:
135 `mem:<len>` memory backed disk
136 <len>: length of ramdisk, e.g.: `1G`
137 `memdiff:<disk>` memory backed diff disk
138 <disk>: lower disk, e.g.: `file:base.img`
139 `file:<path>` file-backed disk
140 <path>: path to file
141
142flags:
143 `ro` open disk as read-only
144 `dvd` specifies that device is cd/dvd and it is read_only
145 `vtl2` assign this disk to VTL2
146 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
147 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
148"#)]
149 #[clap(long, value_name = "FILE")]
150 pub disk: Vec<DiskCli>,
151
152 #[clap(long_help = r#"
154e.g: --nvme 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>` file-backed disk
164 <path>: path to file
165
166flags:
167 `ro` open disk as read-only
168 `vtl2` assign this disk to VTL2
169 `uh` relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
170 `uh-nvme` relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
171
172options:
173 `pcie_port=<name>` present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
174"#)]
175 #[clap(long)]
176 pub nvme: Vec<DiskCli>,
177
178 #[clap(long, value_name = "COUNT", default_value = "0")]
180 pub scsi_sub_channels: u16,
181
182 #[clap(long)]
184 pub nic: bool,
185
186 #[clap(long)]
191 pub net: Vec<NicConfigCli>,
192
193 #[clap(long, value_name = "SWITCH_ID")]
197 pub kernel_vmnic: Vec<String>,
198
199 #[clap(long)]
201 pub gfx: bool,
202
203 #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
205 pub vtl2_gfx: bool,
206
207 #[clap(long)]
209 pub vnc: bool,
210
211 #[clap(long, value_name = "PORT", default_value = "5900")]
213 pub vnc_port: u16,
214
215 #[cfg(guest_arch = "x86_64")]
217 #[clap(long, default_value_t)]
218 pub apic_id_offset: u32,
219
220 #[clap(long)]
222 pub vps_per_socket: Option<u32>,
223
224 #[clap(long, default_value = "auto")]
226 pub smt: SmtConfigCli,
227
228 #[cfg(guest_arch = "x86_64")]
230 #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
231 pub x2apic: X2ApicConfig,
232
233 #[clap(long, value_name = "SERIAL")]
235 pub com1: Option<SerialConfigCli>,
236
237 #[clap(long, value_name = "SERIAL")]
239 pub com2: Option<SerialConfigCli>,
240
241 #[clap(long, value_name = "SERIAL")]
243 pub com3: Option<SerialConfigCli>,
244
245 #[clap(long, value_name = "SERIAL")]
247 pub com4: Option<SerialConfigCli>,
248
249 #[structopt(long, value_name = "SERIAL")]
251 pub vmbus_com1_serial: Option<SerialConfigCli>,
252
253 #[structopt(long, value_name = "SERIAL")]
255 pub vmbus_com2_serial: Option<SerialConfigCli>,
256
257 #[clap(long)]
259 pub serial_tx_only: bool,
260
261 #[clap(long, value_name = "SERIAL")]
263 pub debugcon: Option<DebugconSerialConfigCli>,
264
265 #[clap(long, short = 'e')]
267 pub uefi: bool,
268
269 #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
271 pub uefi_firmware: OptionalPathBuf,
272
273 #[clap(long, requires("uefi"))]
275 pub uefi_debug: bool,
276
277 #[clap(long, requires("uefi"))]
279 pub uefi_enable_memory_protections: bool,
280
281 #[clap(long, requires("pcat"))]
292 pub pcat_boot_order: Option<PcatBootOrderCli>,
293
294 #[clap(long, conflicts_with("uefi"))]
296 pub pcat: bool,
297
298 #[clap(long, requires("pcat"), value_name = "FILE")]
300 pub pcat_firmware: Option<PathBuf>,
301
302 #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
304 pub igvm: Option<PathBuf>,
305
306 #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
309 pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
310
311 #[clap(long, value_name = "tag,root_path")]
313 pub virtio_9p: Vec<FsArgs>,
314
315 #[clap(long)]
317 pub virtio_9p_debug: bool,
318
319 #[clap(long, value_name = "tag,root_path,[options]")]
321 pub virtio_fs: Vec<FsArgsWithOptions>,
322
323 #[clap(long, value_name = "tag,root_path")]
325 pub virtio_fs_shmem: Vec<FsArgs>,
326
327 #[clap(long, value_name = "BUS", default_value = "auto")]
329 pub virtio_fs_bus: VirtioBusCli,
330
331 #[clap(long, value_name = "PATH")]
333 pub virtio_pmem: Option<String>,
334
335 #[clap(long)]
341 pub virtio_net: Vec<NicConfigCli>,
342
343 #[clap(long, value_name = "PATH")]
345 pub log_file: Option<PathBuf>,
346
347 #[clap(long, value_name = "SOCKETPATH")]
349 pub ttrpc: Option<PathBuf>,
350
351 #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
353 pub grpc: Option<PathBuf>,
354
355 #[clap(long)]
357 pub single_process: bool,
358
359 #[cfg(windows)]
361 #[clap(long, value_name = "PATH")]
362 pub device: Vec<String>,
363
364 #[clap(long, requires("uefi"))]
366 pub disable_frontpage: bool,
367
368 #[clap(long)]
370 pub tpm: bool,
371
372 #[clap(long, default_value = "control", hide(true))]
376 #[expect(clippy::option_option)]
377 pub internal_worker: Option<Option<String>>,
378
379 #[clap(long, requires("vtl2"))]
381 pub vmbus_redirect: bool,
382
383 #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
385 pub vmbus_max_version: Option<u32>,
386
387 #[clap(long_help = r#"
391e.g: --vmgs memdiff:file:/path/to/file.vmgs
392
393syntax: <path> | kind:<arg>[,flag]
394
395valid disk kinds:
396 `mem:<len>` memory backed disk
397 <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
398 `memdiff:<disk>[;create=<len>]` memory backed diff disk
399 <disk>: lower disk, e.g.: `file:base.img`
400 `file:<path>` file-backed disk
401 <path>: path to file
402
403flags:
404 `fmt` reprovision the VMGS before boot
405 `fmt-on-fail` reprovision the VMGS before boot if it is corrupted
406"#)]
407 #[clap(long)]
408 pub vmgs: Option<VmgsCli>,
409
410 #[clap(long, requires("vmgs"))]
412 pub test_gsp_by_id: bool,
413
414 #[clap(long, requires("pcat"), value_name = "FILE")]
416 pub vga_firmware: Option<PathBuf>,
417
418 #[clap(long)]
420 pub secure_boot: bool,
421
422 #[clap(long)]
424 pub secure_boot_template: Option<SecureBootTemplateCli>,
425
426 #[clap(long, value_name = "PATH")]
428 pub custom_uefi_json: Option<PathBuf>,
429
430 #[clap(long, hide(true))]
435 pub relay_console_path: Option<PathBuf>,
436
437 #[clap(long, hide(true))]
441 pub relay_console_title: Option<String>,
442
443 #[clap(long, value_name = "PORT")]
445 pub gdb: Option<u16>,
446
447 #[clap(long)]
449 pub mana: Vec<NicConfigCli>,
450
451 #[clap(long, value_parser = parse_hypervisor)]
453 pub hypervisor: Option<Hypervisor>,
454
455 #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
463 pub custom_dsdt: Option<PathBuf>,
464
465 #[clap(long_help = r#"
475e.g: --ide memdiff:file:/path/to/disk.vhd
476
477syntax: <path> | kind:<arg>[,flag,opt=arg,...]
478
479valid disk kinds:
480 `mem:<len>` memory backed disk
481 <len>: length of ramdisk, e.g.: `1G`
482 `memdiff:<disk>` memory backed diff disk
483 <disk>: lower disk, e.g.: `file:base.img`
484 `file:<path>` file-backed disk
485 <path>: path to file
486
487flags:
488 `ro` open disk as read-only
489 `s` attach drive to secondary ide channel
490 `dvd` specifies that device is cd/dvd and it is read_only
491"#)]
492 #[clap(long, value_name = "FILE", requires("pcat"))]
493 pub ide: Vec<IdeDiskCli>,
494
495 #[clap(long_help = r#"
498e.g: --floppy memdiff:/path/to/disk.vfd,ro
499
500syntax: <path> | kind:<arg>[,flag,opt=arg,...]
501
502valid disk kinds:
503 `mem:<len>` memory backed disk
504 <len>: length of ramdisk, e.g.: `1G`
505 `memdiff:<disk>` memory backed diff disk
506 <disk>: lower disk, e.g.: `file:base.img`
507 `file:<path>` file-backed disk
508 <path>: path to file
509
510flags:
511 `ro` open disk as read-only
512"#)]
513 #[clap(long, value_name = "FILE", requires("pcat"))]
514 pub floppy: Vec<FloppyDiskCli>,
515
516 #[clap(long)]
518 pub guest_watchdog: bool,
519
520 #[clap(long)]
522 pub openhcl_dump_path: Option<PathBuf>,
523
524 #[clap(long)]
526 pub halt_on_reset: bool,
527
528 #[clap(long)]
530 pub write_saved_state_proto: Option<PathBuf>,
531
532 #[clap(long)]
534 pub imc: Option<PathBuf>,
535
536 #[clap(long)]
538 pub mcr: bool, #[clap(long)]
542 pub battery: bool,
543
544 #[clap(long)]
546 pub uefi_console_mode: Option<UefiConsoleModeCli>,
547
548 #[clap(long_help = r#"
550Set the EFI diagnostics log level.
551
552options:
553 default default (ERROR and WARN only)
554 info info (ERROR, WARN, and INFO)
555 full full (all log levels)
556"#)]
557 #[clap(long, requires("uefi"))]
558 pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
559
560 #[clap(long)]
562 pub default_boot_always_attempt: bool,
563
564 #[clap(long_help = r#"
566Attach root complexes to the VM.
567
568Examples:
569 # Attach root complex rc0 on segment 0 with bus and MMIO ranges
570 --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
571
572Syntax: <name>[,opt=arg,...]
573
574Options:
575 `segment=<value>` configures the PCI Express segment, default 0
576 `start_bus=<value>` lowest valid bus number, default 0
577 `end_bus=<value>` highest valid bus number, default 255
578 `low_mmio=<size>` low MMIO window size, default 4M
579 `high_mmio=<size>` high MMIO window size, default 1G
580"#)]
581 #[clap(long, conflicts_with("pcat"))]
582 pub pcie_root_complex: Vec<PcieRootComplexCli>,
583
584 #[clap(long_help = r#"
586Attach root ports to root complexes.
587
588Examples:
589 # Attach root port rc0rp0 to root complex rc0
590 --pcie-root-port rc0:rc0rp0
591
592 # Attach root port rc0rp1 to root complex rc0 with hotplug support
593 --pcie-root-port rc0:rc0rp1,hotplug
594
595Syntax: <root_complex_name>:<name>[,hotplug]
596
597Options:
598 `hotplug` enable hotplug support for this root port
599"#)]
600 #[clap(long, conflicts_with("pcat"))]
601 pub pcie_root_port: Vec<PcieRootPortCli>,
602
603 #[clap(long_help = r#"
605Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
606
607Examples:
608 # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
609 --pcie-switch rp0:switch0,num_downstream_ports=4
610
611 # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
612 --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
613
614 # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
615 --pcie-switch rp0:switch0
616 --pcie-switch switch0-downstream-0:switch1
617 --pcie-switch switch1-downstream-1:switch2
618
619 # Enable hotplug on all downstream switch ports of switch0
620 --pcie-switch rp0:switch0,hotplug
621
622Syntax: <port_name>:<name>[,opt,opt=arg,...]
623
624 port_name can be:
625 - Root port name (e.g., "rp0") to connect directly to a root port
626 - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
627
628Options:
629 `hotplug` enable hotplug support for all downstream switch ports
630 `num_downstream_ports=<value>` number of downstream ports, default 4
631"#)]
632 #[clap(long, conflicts_with("pcat"))]
633 pub pcie_switch: Vec<GenericPcieSwitchCli>,
634
635 #[clap(long_help = r#"
637Attach PCIe devices to root ports or downstream switch ports
638which are implemented in a simulator running in a remote process.
639
640Examples:
641 # Attach to root port rc0rp0 with default socket
642 --pcie-remote rc0rp0
643
644 # Attach with custom socket path
645 --pcie-remote rc0rp0,socket=/tmp/custom.sock
646
647 # Specify HU and controller identifiers
648 --pcie-remote rc0rp0,hu=1,controller=0
649
650 # Multiple devices on different ports
651 --pcie-remote rc0rp0,socket=/tmp/dev0.sock
652 --pcie-remote rc0rp1,socket=/tmp/dev1.sock
653
654Syntax: <port_name>[,opt=arg,...]
655
656Options:
657 `socket=<path>` Unix socket path (default: /tmp/qemu-pci-remote-0-ep.sock)
658 `hu=<value>` Hardware unit identifier (default: 0)
659 `controller=<value>` Controller identifier (default: 0)
660"#)]
661 #[clap(long, conflicts_with("pcat"))]
662 pub pcie_remote: Vec<PcieRemoteCli>,
663}
664
665#[derive(Clone, Debug, PartialEq)]
666pub struct FsArgs {
667 pub tag: String,
668 pub path: String,
669}
670
671impl FromStr for FsArgs {
672 type Err = anyhow::Error;
673
674 fn from_str(s: &str) -> Result<Self, Self::Err> {
675 let mut s = s.split(',');
676 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
677 anyhow::bail!("expected <tag>,<path>");
678 };
679 Ok(Self {
680 tag: tag.to_owned(),
681 path: path.to_owned(),
682 })
683 }
684}
685
686#[derive(Clone, Debug, PartialEq)]
687pub struct FsArgsWithOptions {
688 pub tag: String,
690 pub path: String,
692 pub options: String,
694}
695
696impl FromStr for FsArgsWithOptions {
697 type Err = anyhow::Error;
698
699 fn from_str(s: &str) -> Result<Self, Self::Err> {
700 let mut s = s.split(',');
701 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
702 anyhow::bail!("expected <tag>,<path>[,<options>]");
703 };
704 let options = s.collect::<Vec<_>>().join(";");
705 Ok(Self {
706 tag: tag.to_owned(),
707 path: path.to_owned(),
708 options,
709 })
710 }
711}
712
713#[derive(Copy, Clone, clap::ValueEnum)]
714pub enum VirtioBusCli {
715 Auto,
716 Mmio,
717 Pci,
718 Vpci,
719}
720
721#[derive(clap::ValueEnum, Clone, Copy)]
722pub enum SecureBootTemplateCli {
723 Windows,
724 UefiCa,
725}
726
727fn parse_memory(s: &str) -> anyhow::Result<u64> {
728 if s == "VMGS_DEFAULT" {
729 Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
730 } else {
731 || -> Option<u64> {
732 let mut b = s.as_bytes();
733 if s.ends_with('B') {
734 b = &b[..b.len() - 1]
735 }
736 if b.is_empty() {
737 return None;
738 }
739 let multi = match b[b.len() - 1] as char {
740 'T' => Some(1024 * 1024 * 1024 * 1024),
741 'G' => Some(1024 * 1024 * 1024),
742 'M' => Some(1024 * 1024),
743 'K' => Some(1024),
744 _ => None,
745 };
746 if multi.is_some() {
747 b = &b[..b.len() - 1]
748 }
749 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
750 Some(n * multi.unwrap_or(1))
751 }()
752 .with_context(|| format!("invalid memory size '{0}'", s))
753 }
754}
755
756fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
758 match s.strip_prefix("0x") {
759 Some(rest) => u64::from_str_radix(rest, 16),
760 None => s.parse::<u64>(),
761 }
762}
763
764#[derive(Clone, Debug, PartialEq)]
765pub enum DiskCliKind {
766 Memory(u64),
768 MemoryDiff(Box<DiskCliKind>),
770 Sqlite {
772 path: PathBuf,
773 create_with_len: Option<u64>,
774 },
775 SqliteDiff {
777 path: PathBuf,
778 create: bool,
779 disk: Box<DiskCliKind>,
780 },
781 AutoCacheSqlite {
783 cache_path: String,
784 key: Option<String>,
785 disk: Box<DiskCliKind>,
786 },
787 PersistentReservationsWrapper(Box<DiskCliKind>),
789 File {
791 path: PathBuf,
792 create_with_len: Option<u64>,
793 },
794 Blob {
796 kind: BlobKind,
797 url: String,
798 },
799 Crypt {
801 cipher: DiskCipher,
802 key_file: PathBuf,
803 disk: Box<DiskCliKind>,
804 },
805 DelayDiskWrapper {
807 delay_ms: u64,
808 disk: Box<DiskCliKind>,
809 },
810}
811
812#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
813pub enum DiskCipher {
814 #[clap(name = "xts-aes-256")]
815 XtsAes256,
816}
817
818#[derive(Copy, Clone, Debug, PartialEq)]
819pub enum BlobKind {
820 Flat,
821 Vhd1,
822}
823
824fn parse_path_and_len(arg: &str) -> anyhow::Result<(PathBuf, Option<u64>)> {
825 Ok(match arg.split_once(';') {
826 Some((path, len)) => {
827 let Some(len) = len.strip_prefix("create=") else {
828 anyhow::bail!("invalid syntax after ';', expected 'create=<len>'")
829 };
830
831 let len = parse_memory(len)?;
832
833 (path.into(), Some(len))
834 }
835 None => (arg.into(), None),
836 })
837}
838
839impl FromStr for DiskCliKind {
840 type Err = anyhow::Error;
841
842 fn from_str(s: &str) -> anyhow::Result<Self> {
843 let disk = match s.split_once(':') {
844 None => {
846 let (path, create_with_len) = parse_path_and_len(s)?;
847 DiskCliKind::File {
848 path,
849 create_with_len,
850 }
851 }
852 Some((kind, arg)) => match kind {
853 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
854 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
855 "sql" => {
856 let (path, create_with_len) = parse_path_and_len(arg)?;
857 DiskCliKind::Sqlite {
858 path,
859 create_with_len,
860 }
861 }
862 "sqldiff" => {
863 let (path_and_opts, kind) =
864 arg.split_once(':').context("expected path[;opts]:kind")?;
865 let disk = Box::new(kind.parse()?);
866 match path_and_opts.split_once(';') {
867 Some((path, create)) => {
868 if create != "create" {
869 anyhow::bail!("invalid syntax after ';', expected 'create'")
870 }
871 DiskCliKind::SqliteDiff {
872 path: path.into(),
873 create: true,
874 disk,
875 }
876 }
877 None => DiskCliKind::SqliteDiff {
878 path: path_and_opts.into(),
879 create: false,
880 disk,
881 },
882 }
883 }
884 "autocache" => {
885 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
886 let cache_path = std::env::var("OPENVMM_AUTO_CACHE_PATH")
887 .context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
888 DiskCliKind::AutoCacheSqlite {
889 cache_path,
890 key: (!key.is_empty()).then(|| key.to_string()),
891 disk: Box::new(kind.parse()?),
892 }
893 }
894 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
895 "file" => {
896 let (path, create_with_len) = parse_path_and_len(arg)?;
897 DiskCliKind::File {
898 path,
899 create_with_len,
900 }
901 }
902 "blob" => {
903 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
904 let blob_kind = match blob_kind {
905 "flat" => BlobKind::Flat,
906 "vhd1" => BlobKind::Vhd1,
907 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
908 };
909 DiskCliKind::Blob {
910 kind: blob_kind,
911 url: url.to_string(),
912 }
913 }
914 "crypt" => {
915 let (cipher, (key, kind)) = arg
916 .split_once(':')
917 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
918 .context("expected cipher:key_file:kind")?;
919 DiskCliKind::Crypt {
920 cipher: ValueEnum::from_str(cipher, false)
921 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
922 key_file: PathBuf::from(key),
923 disk: Box::new(kind.parse()?),
924 }
925 }
926 kind => {
927 let (path, create_with_len) = parse_path_and_len(s)?;
932 if path.has_root() {
933 DiskCliKind::File {
934 path,
935 create_with_len,
936 }
937 } else {
938 anyhow::bail!("invalid disk kind {kind}");
939 }
940 }
941 },
942 };
943 Ok(disk)
944 }
945}
946
947#[derive(Clone)]
948pub struct VmgsCli {
949 pub kind: DiskCliKind,
950 pub provision: ProvisionVmgs,
951}
952
953#[derive(Copy, Clone)]
954pub enum ProvisionVmgs {
955 OnEmpty,
956 OnFailure,
957 True,
958}
959
960impl FromStr for VmgsCli {
961 type Err = anyhow::Error;
962
963 fn from_str(s: &str) -> anyhow::Result<Self> {
964 let (kind, opt) = s
965 .split_once(',')
966 .map(|(k, o)| (k, Some(o)))
967 .unwrap_or((s, None));
968 let kind = kind.parse()?;
969
970 let provision = match opt {
971 None => ProvisionVmgs::OnEmpty,
972 Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
973 Some("fmt") => ProvisionVmgs::True,
974 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
975 };
976
977 Ok(VmgsCli { kind, provision })
978 }
979}
980
981#[derive(Clone)]
983pub struct DiskCli {
984 pub vtl: DeviceVtl,
985 pub kind: DiskCliKind,
986 pub read_only: bool,
987 pub is_dvd: bool,
988 pub underhill: Option<UnderhillDiskSource>,
989 pub pcie_port: Option<String>,
990}
991
992#[derive(Copy, Clone)]
993pub enum UnderhillDiskSource {
994 Scsi,
995 Nvme,
996}
997
998impl FromStr for DiskCli {
999 type Err = anyhow::Error;
1000
1001 fn from_str(s: &str) -> anyhow::Result<Self> {
1002 let mut opts = s.split(',');
1003 let kind = opts.next().unwrap().parse()?;
1004
1005 let mut read_only = false;
1006 let mut is_dvd = false;
1007 let mut underhill = None;
1008 let mut vtl = DeviceVtl::Vtl0;
1009 let mut pcie_port = None;
1010 for opt in opts {
1011 let mut s = opt.split('=');
1012 let opt = s.next().unwrap();
1013 match opt {
1014 "ro" => read_only = true,
1015 "dvd" => {
1016 is_dvd = true;
1017 read_only = true;
1018 }
1019 "vtl2" => {
1020 vtl = DeviceVtl::Vtl2;
1021 }
1022 "uh" => underhill = Some(UnderhillDiskSource::Scsi),
1023 "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
1024 "pcie_port" => {
1025 let port = s.next();
1026 if port.is_none_or(|p| p.is_empty()) {
1027 anyhow::bail!("`pcie_port` requires a port name");
1028 }
1029 pcie_port = Some(String::from(port.unwrap()));
1030 }
1031 opt => anyhow::bail!("unknown option: '{opt}'"),
1032 }
1033 }
1034
1035 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
1036 anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
1037 }
1038
1039 if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
1040 anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
1041 }
1042
1043 Ok(DiskCli {
1044 vtl,
1045 kind,
1046 read_only,
1047 is_dvd,
1048 underhill,
1049 pcie_port,
1050 })
1051 }
1052}
1053
1054#[derive(Clone)]
1056pub struct IdeDiskCli {
1057 pub kind: DiskCliKind,
1058 pub read_only: bool,
1059 pub channel: Option<u8>,
1060 pub device: Option<u8>,
1061 pub is_dvd: bool,
1062}
1063
1064impl FromStr for IdeDiskCli {
1065 type Err = anyhow::Error;
1066
1067 fn from_str(s: &str) -> anyhow::Result<Self> {
1068 let mut opts = s.split(',');
1069 let kind = opts.next().unwrap().parse()?;
1070
1071 let mut read_only = false;
1072 let mut channel = None;
1073 let mut device = None;
1074 let mut is_dvd = false;
1075 for opt in opts {
1076 let mut s = opt.split('=');
1077 let opt = s.next().unwrap();
1078 match opt {
1079 "ro" => read_only = true,
1080 "p" => channel = Some(0),
1081 "s" => channel = Some(1),
1082 "0" => device = Some(0),
1083 "1" => device = Some(1),
1084 "dvd" => {
1085 is_dvd = true;
1086 read_only = true;
1087 }
1088 _ => anyhow::bail!("unknown option: '{opt}'"),
1089 }
1090 }
1091
1092 Ok(IdeDiskCli {
1093 kind,
1094 read_only,
1095 channel,
1096 device,
1097 is_dvd,
1098 })
1099 }
1100}
1101
1102#[derive(Clone, Debug, PartialEq)]
1104pub struct FloppyDiskCli {
1105 pub kind: DiskCliKind,
1106 pub read_only: bool,
1107}
1108
1109impl FromStr for FloppyDiskCli {
1110 type Err = anyhow::Error;
1111
1112 fn from_str(s: &str) -> anyhow::Result<Self> {
1113 if s.is_empty() {
1114 anyhow::bail!("empty disk spec");
1115 }
1116 let mut opts = s.split(',');
1117 let kind = opts.next().unwrap().parse()?;
1118
1119 let mut read_only = false;
1120 for opt in opts {
1121 let mut s = opt.split('=');
1122 let opt = s.next().unwrap();
1123 match opt {
1124 "ro" => read_only = true,
1125 _ => anyhow::bail!("unknown option: '{opt}'"),
1126 }
1127 }
1128
1129 Ok(FloppyDiskCli { kind, read_only })
1130 }
1131}
1132
1133#[derive(Clone)]
1134pub struct DebugconSerialConfigCli {
1135 pub port: u16,
1136 pub serial: SerialConfigCli,
1137}
1138
1139impl FromStr for DebugconSerialConfigCli {
1140 type Err = String;
1141
1142 fn from_str(s: &str) -> Result<Self, Self::Err> {
1143 let Some((port, serial)) = s.split_once(',') else {
1144 return Err("invalid format (missing comma between port and serial)".into());
1145 };
1146
1147 let port: u16 = parse_number(port)
1148 .map_err(|_| "could not parse port".to_owned())?
1149 .try_into()
1150 .map_err(|_| "port must be 16-bit")?;
1151 let serial: SerialConfigCli = serial.parse()?;
1152
1153 Ok(Self { port, serial })
1154 }
1155}
1156
1157#[derive(Clone, Debug, PartialEq)]
1159pub enum SerialConfigCli {
1160 None,
1161 Console,
1162 NewConsole(Option<PathBuf>, Option<String>),
1163 Stderr,
1164 Pipe(PathBuf),
1165 Tcp(SocketAddr),
1166 File(PathBuf),
1167}
1168
1169impl FromStr for SerialConfigCli {
1170 type Err = String;
1171
1172 fn from_str(s: &str) -> Result<Self, Self::Err> {
1173 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1174
1175 let first_key = match keyvalues.first() {
1176 Some(first_pair) => first_pair.0.as_str(),
1177 None => Err("invalid serial configuration: no values supplied")?,
1178 };
1179 let first_value = keyvalues.first().unwrap().1.as_ref();
1180
1181 let ret = match first_key {
1182 "none" => SerialConfigCli::None,
1183 "console" => SerialConfigCli::Console,
1184 "stderr" => SerialConfigCli::Stderr,
1185 "file" => match first_value {
1186 Some(path) => SerialConfigCli::File(path.into()),
1187 None => Err("invalid serial configuration: file requires a value")?,
1188 },
1189 "term" => {
1190 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1192 let window_name = match window_name {
1193 Some((_, Some(name))) => Some(name.clone()),
1194 _ => None,
1195 };
1196
1197 SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
1198 }
1199 "listen" => match first_value {
1200 Some(path) => {
1201 if let Some(tcp) = path.strip_prefix("tcp:") {
1202 let addr = tcp
1203 .parse()
1204 .map_err(|err| format!("invalid tcp address: {err}"))?;
1205 SerialConfigCli::Tcp(addr)
1206 } else {
1207 SerialConfigCli::Pipe(path.into())
1208 }
1209 }
1210 None => Err(
1211 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1212 )?,
1213 },
1214 _ => {
1215 return Err(format!(
1216 "invalid serial configuration: '{}' is not a known option",
1217 first_key
1218 ));
1219 }
1220 };
1221
1222 Ok(ret)
1223 }
1224}
1225
1226impl SerialConfigCli {
1227 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1230 let mut ret = Vec::new();
1231
1232 for item in s.split(',') {
1234 let mut eqsplit = item.split('=');
1237 let key = eqsplit.next();
1238 let value = eqsplit.next();
1239
1240 if let Some(key) = key {
1241 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1242 } else {
1243 return Err("invalid key=value pair in serial config".into());
1245 }
1246 }
1247 Ok(ret)
1248 }
1249}
1250
1251#[derive(Clone, Debug, PartialEq)]
1252pub enum EndpointConfigCli {
1253 None,
1254 Consomme { cidr: Option<String> },
1255 Dio { id: Option<String> },
1256 Tap { name: String },
1257}
1258
1259impl FromStr for EndpointConfigCli {
1260 type Err = String;
1261
1262 fn from_str(s: &str) -> Result<Self, Self::Err> {
1263 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1264 ["none"] => EndpointConfigCli::None,
1265 ["consomme", s @ ..] => EndpointConfigCli::Consomme {
1266 cidr: s.first().map(|&s| s.to_owned()),
1267 },
1268 ["dio", s @ ..] => EndpointConfigCli::Dio {
1269 id: s.first().map(|s| (*s).to_owned()),
1270 },
1271 ["tap", name] => EndpointConfigCli::Tap {
1272 name: (*name).to_owned(),
1273 },
1274 _ => return Err("invalid network backend".into()),
1275 };
1276
1277 Ok(ret)
1278 }
1279}
1280
1281#[derive(Clone, Debug, PartialEq)]
1282pub struct NicConfigCli {
1283 pub vtl: DeviceVtl,
1284 pub endpoint: EndpointConfigCli,
1285 pub max_queues: Option<u16>,
1286 pub underhill: bool,
1287}
1288
1289impl FromStr for NicConfigCli {
1290 type Err = String;
1291
1292 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1293 let mut vtl = DeviceVtl::Vtl0;
1294 let mut max_queues = None;
1295 let mut underhill = false;
1296 while let Some((opt, rest)) = s.split_once(':') {
1297 if let Some((opt, val)) = opt.split_once('=') {
1298 match opt {
1299 "queues" => {
1300 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1301 }
1302 _ => break,
1303 }
1304 } else {
1305 match opt {
1306 "vtl2" => {
1307 vtl = DeviceVtl::Vtl2;
1308 }
1309 "uh" => underhill = true,
1310 _ => break,
1311 }
1312 }
1313 s = rest;
1314 }
1315
1316 if underhill && vtl != DeviceVtl::Vtl0 {
1317 return Err("`uh` is incompatible with `vtl2`".into());
1318 }
1319
1320 let endpoint = s.parse()?;
1321 Ok(NicConfigCli {
1322 vtl,
1323 endpoint,
1324 max_queues,
1325 underhill,
1326 })
1327 }
1328}
1329
1330#[derive(Debug, Error)]
1331#[error("unknown hypervisor: {0}")]
1332pub struct UnknownHypervisor(String);
1333
1334fn parse_hypervisor(s: &str) -> Result<Hypervisor, UnknownHypervisor> {
1335 match s {
1336 "kvm" => Ok(Hypervisor::Kvm),
1337 "mshv" => Ok(Hypervisor::MsHv),
1338 "whp" => Ok(Hypervisor::Whp),
1339 _ => Err(UnknownHypervisor(s.to_owned())),
1340 }
1341}
1342
1343#[derive(Debug, Error)]
1344#[error("unknown VTL2 relocation type: {0}")]
1345pub struct UnknownVtl2RelocationType(String);
1346
1347fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1348 match s {
1349 "disable" => Ok(Vtl2BaseAddressType::File),
1350 s if s.starts_with("auto=") => {
1351 let s = s.strip_prefix("auto=").unwrap_or_default();
1352 let size = if s == "filesize" {
1353 None
1354 } else {
1355 let size = parse_memory(s).map_err(|e| {
1356 UnknownVtl2RelocationType(format!(
1357 "unable to parse memory size from {} for 'auto=' type, {e}",
1358 e
1359 ))
1360 })?;
1361 Some(size)
1362 };
1363 Ok(Vtl2BaseAddressType::MemoryLayout { size })
1364 }
1365 s if s.starts_with("absolute=") => {
1366 let s = s.strip_prefix("absolute=");
1367 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
1368 UnknownVtl2RelocationType(format!(
1369 "unable to parse number from {} for 'absolute=' type",
1370 e
1371 ))
1372 })?;
1373 Ok(Vtl2BaseAddressType::Absolute(addr))
1374 }
1375 s if s.starts_with("vtl2=") => {
1376 let s = s.strip_prefix("vtl2=").unwrap_or_default();
1377 let size = if s == "filesize" {
1378 None
1379 } else {
1380 let size = parse_memory(s).map_err(|e| {
1381 UnknownVtl2RelocationType(format!(
1382 "unable to parse memory size from {} for 'vtl2=' type, {e}",
1383 e
1384 ))
1385 })?;
1386 Some(size)
1387 };
1388 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
1389 }
1390 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
1391 }
1392}
1393
1394#[derive(Debug, Copy, Clone, PartialEq)]
1395pub enum SmtConfigCli {
1396 Auto,
1397 Force,
1398 Off,
1399}
1400
1401#[derive(Debug, Error)]
1402#[error("expected auto, force, or off")]
1403pub struct BadSmtConfig;
1404
1405impl FromStr for SmtConfigCli {
1406 type Err = BadSmtConfig;
1407
1408 fn from_str(s: &str) -> Result<Self, Self::Err> {
1409 let r = match s {
1410 "auto" => Self::Auto,
1411 "force" => Self::Force,
1412 "off" => Self::Off,
1413 _ => return Err(BadSmtConfig),
1414 };
1415 Ok(r)
1416 }
1417}
1418
1419#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
1420fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
1421 let r = match s {
1422 "auto" => X2ApicConfig::Auto,
1423 "supported" => X2ApicConfig::Supported,
1424 "off" => X2ApicConfig::Unsupported,
1425 "on" => X2ApicConfig::Enabled,
1426 _ => return Err("expected auto, supported, off, or on"),
1427 };
1428 Ok(r)
1429}
1430
1431#[derive(Debug, Copy, Clone, ValueEnum)]
1432pub enum Vtl0LateMapPolicyCli {
1433 Off,
1434 Log,
1435 Halt,
1436 Exception,
1437}
1438
1439#[derive(Debug, Copy, Clone, ValueEnum)]
1440pub enum IsolationCli {
1441 Vbs,
1442}
1443
1444#[derive(Debug, Copy, Clone, PartialEq)]
1445pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
1446
1447impl FromStr for PcatBootOrderCli {
1448 type Err = &'static str;
1449
1450 fn from_str(s: &str) -> Result<Self, Self::Err> {
1451 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
1452 let mut order = Vec::new();
1453
1454 for item in s.split(',') {
1455 let device = match item {
1456 "optical" => PcatBootDevice::Optical,
1457 "hdd" => PcatBootDevice::HardDrive,
1458 "net" => PcatBootDevice::Network,
1459 "floppy" => PcatBootDevice::Floppy,
1460 _ => return Err("unknown boot device type"),
1461 };
1462
1463 let default_pos = default_order
1464 .iter()
1465 .position(|x| x == &Some(device))
1466 .ok_or("cannot pass duplicate boot devices")?;
1467
1468 order.push(default_order[default_pos].take().unwrap());
1469 }
1470
1471 order.extend(default_order.into_iter().flatten());
1472 assert_eq!(order.len(), 4);
1473
1474 Ok(Self(order.try_into().unwrap()))
1475 }
1476}
1477
1478#[derive(Copy, Clone, Debug, ValueEnum)]
1479pub enum UefiConsoleModeCli {
1480 Default,
1481 Com1,
1482 Com2,
1483 None,
1484}
1485
1486#[derive(Copy, Clone, Debug, Default, ValueEnum)]
1487pub enum EfiDiagnosticsLogLevelCli {
1488 #[default]
1489 Default,
1490 Info,
1491 Full,
1492}
1493
1494#[derive(Clone, Debug, PartialEq)]
1495pub struct PcieRootComplexCli {
1496 pub name: String,
1497 pub segment: u16,
1498 pub start_bus: u8,
1499 pub end_bus: u8,
1500 pub low_mmio: u32,
1501 pub high_mmio: u64,
1502}
1503
1504impl FromStr for PcieRootComplexCli {
1505 type Err = anyhow::Error;
1506
1507 fn from_str(s: &str) -> Result<Self, Self::Err> {
1508 const DEFAULT_PCIE_CRS_LOW_SIZE: u32 = 4 * 1024 * 1024; const DEFAULT_PCIE_CRS_HIGH_SIZE: u64 = 1024 * 1024 * 1024; let mut opts = s.split(',');
1512 let name = opts.next().context("expected root complex name")?;
1513 if name.is_empty() {
1514 anyhow::bail!("must provide a root complex name");
1515 }
1516
1517 let mut segment = 0;
1518 let mut start_bus = 0;
1519 let mut end_bus = 255;
1520 let mut low_mmio = DEFAULT_PCIE_CRS_LOW_SIZE;
1521 let mut high_mmio = DEFAULT_PCIE_CRS_HIGH_SIZE;
1522 for opt in opts {
1523 let mut s = opt.split('=');
1524 let opt = s.next().context("expected option")?;
1525 match opt {
1526 "segment" => {
1527 let seg_str = s.next().context("expected segment number")?;
1528 segment = u16::from_str(seg_str).context("failed to parse segment number")?;
1529 }
1530 "start_bus" => {
1531 let bus_str = s.next().context("expected start bus number")?;
1532 start_bus =
1533 u8::from_str(bus_str).context("failed to parse start bus number")?;
1534 }
1535 "end_bus" => {
1536 let bus_str = s.next().context("expected end bus number")?;
1537 end_bus = u8::from_str(bus_str).context("failed to parse end bus number")?;
1538 }
1539 "low_mmio" => {
1540 let low_mmio_str = s.next().context("expected low MMIO size")?;
1541 low_mmio = parse_memory(low_mmio_str)
1542 .context("failed to parse low MMIO size")?
1543 .try_into()?;
1544 }
1545 "high_mmio" => {
1546 let high_mmio_str = s.next().context("expected high MMIO size")?;
1547 high_mmio =
1548 parse_memory(high_mmio_str).context("failed to parse high MMIO size")?;
1549 }
1550 opt => anyhow::bail!("unknown option: '{opt}'"),
1551 }
1552 }
1553
1554 if start_bus >= end_bus {
1555 anyhow::bail!("start_bus must be less than or equal to end_bus");
1556 }
1557
1558 Ok(PcieRootComplexCli {
1559 name: name.to_string(),
1560 segment,
1561 start_bus,
1562 end_bus,
1563 low_mmio,
1564 high_mmio,
1565 })
1566 }
1567}
1568
1569#[derive(Clone, Debug, PartialEq)]
1570pub struct PcieRootPortCli {
1571 pub root_complex_name: String,
1572 pub name: String,
1573 pub hotplug: bool,
1574}
1575
1576impl FromStr for PcieRootPortCli {
1577 type Err = anyhow::Error;
1578
1579 fn from_str(s: &str) -> Result<Self, Self::Err> {
1580 let mut opts = s.split(',');
1581 let names = opts.next().context("expected root port identifiers")?;
1582 if names.is_empty() {
1583 anyhow::bail!("must provide root port identifiers");
1584 }
1585
1586 let mut s = names.split(':');
1587 let rc_name = s.next().context("expected name of parent root complex")?;
1588 let rp_name = s.next().context("expected root port name")?;
1589
1590 if let Some(extra) = s.next() {
1591 anyhow::bail!("unexpected token: '{extra}'")
1592 }
1593
1594 let mut hotplug = false;
1595
1596 for opt in opts {
1598 match opt {
1599 "hotplug" => hotplug = true,
1600 _ => anyhow::bail!("unexpected option: '{opt}'"),
1601 }
1602 }
1603
1604 Ok(PcieRootPortCli {
1605 root_complex_name: rc_name.to_string(),
1606 name: rp_name.to_string(),
1607 hotplug,
1608 })
1609 }
1610}
1611
1612#[derive(Clone, Debug, PartialEq)]
1613pub struct GenericPcieSwitchCli {
1614 pub port_name: String,
1615 pub name: String,
1616 pub num_downstream_ports: u8,
1617 pub hotplug: bool,
1618}
1619
1620impl FromStr for GenericPcieSwitchCli {
1621 type Err = anyhow::Error;
1622
1623 fn from_str(s: &str) -> Result<Self, Self::Err> {
1624 let mut opts = s.split(',');
1625 let names = opts.next().context("expected switch identifiers")?;
1626 if names.is_empty() {
1627 anyhow::bail!("must provide switch identifiers");
1628 }
1629
1630 let mut s = names.split(':');
1631 let port_name = s.next().context("expected name of parent port")?;
1632 let switch_name = s.next().context("expected switch name")?;
1633
1634 if let Some(extra) = s.next() {
1635 anyhow::bail!("unexpected token: '{extra}'")
1636 }
1637
1638 let mut num_downstream_ports = 4u8; let mut hotplug = false;
1640
1641 for opt in opts {
1642 let mut kv = opt.split('=');
1643 let key = kv.next().context("expected option name")?;
1644
1645 match key {
1646 "num_downstream_ports" => {
1647 let value = kv.next().context("expected option value")?;
1648 if let Some(extra) = kv.next() {
1649 anyhow::bail!("unexpected token: '{extra}'")
1650 }
1651 num_downstream_ports = value.parse().context("invalid num_downstream_ports")?;
1652 }
1653 "hotplug" => {
1654 if kv.next().is_some() {
1655 anyhow::bail!("hotplug option does not take a value")
1656 }
1657 hotplug = true;
1658 }
1659 _ => anyhow::bail!("unknown option: '{key}'"),
1660 }
1661 }
1662
1663 Ok(GenericPcieSwitchCli {
1664 port_name: port_name.to_string(),
1665 name: switch_name.to_string(),
1666 num_downstream_ports,
1667 hotplug,
1668 })
1669 }
1670}
1671
1672#[derive(Clone, Debug, PartialEq)]
1674pub struct PcieRemoteCli {
1675 pub port_name: String,
1677 pub socket_path: Option<String>,
1679 pub hu: u16,
1681 pub controller: u16,
1683}
1684
1685impl FromStr for PcieRemoteCli {
1686 type Err = anyhow::Error;
1687
1688 fn from_str(s: &str) -> Result<Self, Self::Err> {
1689 let mut opts = s.split(',');
1690 let port_name = opts.next().context("expected port name")?;
1691 if port_name.is_empty() {
1692 anyhow::bail!("must provide a port name");
1693 }
1694
1695 let mut socket_path = None;
1696 let mut hu = 0u16;
1697 let mut controller = 0u16;
1698
1699 for opt in opts {
1700 let mut kv = opt.split('=');
1701 let key = kv.next().context("expected option name")?;
1702 let value = kv.next();
1703
1704 match key {
1705 "socket" => {
1706 let path = value.context("socket requires a path")?;
1707 if let Some(extra) = kv.next() {
1708 anyhow::bail!("unexpected token: '{extra}'")
1709 }
1710 if path.is_empty() {
1711 anyhow::bail!("socket path cannot be empty");
1712 }
1713 socket_path = Some(path.to_string());
1714 }
1715 "hu" => {
1716 let val = value.context("hu requires a value")?;
1717 if let Some(extra) = kv.next() {
1718 anyhow::bail!("unexpected token: '{extra}'")
1719 }
1720 hu = val.parse().context("failed to parse hu")?;
1721 }
1722 "controller" => {
1723 let val = value.context("controller requires a value")?;
1724 if let Some(extra) = kv.next() {
1725 anyhow::bail!("unexpected token: '{extra}'")
1726 }
1727 controller = val.parse().context("failed to parse controller")?;
1728 }
1729 _ => anyhow::bail!("unknown option: '{key}'"),
1730 }
1731 }
1732
1733 Ok(PcieRemoteCli {
1734 port_name: port_name.to_string(),
1735 socket_path,
1736 hu,
1737 controller,
1738 })
1739 }
1740}
1741
1742fn default_value_from_arch_env(name: &str) -> OsString {
1750 let prefix = if cfg!(guest_arch = "x86_64") {
1751 "X86_64"
1752 } else if cfg!(guest_arch = "aarch64") {
1753 "AARCH64"
1754 } else {
1755 return Default::default();
1756 };
1757 let prefixed = format!("{}_{}", prefix, name);
1758 std::env::var_os(name)
1759 .or_else(|| std::env::var_os(prefixed))
1760 .unwrap_or_default()
1761}
1762
1763#[derive(Clone)]
1765pub struct OptionalPathBuf(pub Option<PathBuf>);
1766
1767impl From<&std::ffi::OsStr> for OptionalPathBuf {
1768 fn from(s: &std::ffi::OsStr) -> Self {
1769 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
1770 }
1771}
1772
1773#[cfg(test)]
1774#[expect(unsafe_code)]
1776mod tests {
1777 use super::*;
1778
1779 fn with_env_var<F, R>(name: &str, value: &str, f: F) -> R
1780 where
1781 F: FnOnce() -> R,
1782 {
1783 unsafe {
1786 std::env::set_var(name, value);
1787 }
1788 let result = f();
1789 unsafe {
1792 std::env::remove_var(name);
1793 }
1794 result
1795 }
1796
1797 #[test]
1798 fn test_parse_file_disk_with_create() {
1799 let s = "file:test.vhd;create=1G";
1800 let disk = DiskCliKind::from_str(s).unwrap();
1801
1802 match disk {
1803 DiskCliKind::File {
1804 path,
1805 create_with_len,
1806 } => {
1807 assert_eq!(path, PathBuf::from("test.vhd"));
1808 assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); }
1810 _ => panic!("Expected File variant"),
1811 }
1812 }
1813
1814 #[test]
1815 fn test_parse_direct_file_with_create() {
1816 let s = "test.vhd;create=1G";
1817 let disk = DiskCliKind::from_str(s).unwrap();
1818
1819 match disk {
1820 DiskCliKind::File {
1821 path,
1822 create_with_len,
1823 } => {
1824 assert_eq!(path, PathBuf::from("test.vhd"));
1825 assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); }
1827 _ => panic!("Expected File variant"),
1828 }
1829 }
1830
1831 #[test]
1832 fn test_parse_memory_disk() {
1833 let s = "mem:1G";
1834 let disk = DiskCliKind::from_str(s).unwrap();
1835 match disk {
1836 DiskCliKind::Memory(size) => {
1837 assert_eq!(size, 1024 * 1024 * 1024); }
1839 _ => panic!("Expected Memory variant"),
1840 }
1841 }
1842
1843 #[test]
1844 fn test_parse_pcie_disk() {
1845 assert_eq!(
1846 DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
1847 Some("p0".to_string())
1848 );
1849 assert_eq!(
1850 DiskCli::from_str("file:path.vhdx,pcie_port=p0")
1851 .unwrap()
1852 .pcie_port,
1853 Some("p0".to_string())
1854 );
1855 assert_eq!(
1856 DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
1857 .unwrap()
1858 .pcie_port,
1859 Some("p0".to_string())
1860 );
1861
1862 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
1864
1865 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
1867 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
1868 assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
1869 }
1870
1871 #[test]
1872 fn test_parse_memory_diff_disk() {
1873 let s = "memdiff:file:base.img";
1874 let disk = DiskCliKind::from_str(s).unwrap();
1875 match disk {
1876 DiskCliKind::MemoryDiff(inner) => match *inner {
1877 DiskCliKind::File {
1878 path,
1879 create_with_len,
1880 } => {
1881 assert_eq!(path, PathBuf::from("base.img"));
1882 assert_eq!(create_with_len, None);
1883 }
1884 _ => panic!("Expected File variant inside MemoryDiff"),
1885 },
1886 _ => panic!("Expected MemoryDiff variant"),
1887 }
1888 }
1889
1890 #[test]
1891 fn test_parse_sqlite_disk() {
1892 let s = "sql:db.sqlite;create=2G";
1893 let disk = DiskCliKind::from_str(s).unwrap();
1894 match disk {
1895 DiskCliKind::Sqlite {
1896 path,
1897 create_with_len,
1898 } => {
1899 assert_eq!(path, PathBuf::from("db.sqlite"));
1900 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
1901 }
1902 _ => panic!("Expected Sqlite variant"),
1903 }
1904
1905 let s = "sql:db.sqlite";
1907 let disk = DiskCliKind::from_str(s).unwrap();
1908 match disk {
1909 DiskCliKind::Sqlite {
1910 path,
1911 create_with_len,
1912 } => {
1913 assert_eq!(path, PathBuf::from("db.sqlite"));
1914 assert_eq!(create_with_len, None);
1915 }
1916 _ => panic!("Expected Sqlite variant"),
1917 }
1918 }
1919
1920 #[test]
1921 fn test_parse_sqlite_diff_disk() {
1922 let s = "sqldiff:diff.sqlite;create:file:base.img";
1924 let disk = DiskCliKind::from_str(s).unwrap();
1925 match disk {
1926 DiskCliKind::SqliteDiff { path, create, disk } => {
1927 assert_eq!(path, PathBuf::from("diff.sqlite"));
1928 assert!(create);
1929 match *disk {
1930 DiskCliKind::File {
1931 path,
1932 create_with_len,
1933 } => {
1934 assert_eq!(path, PathBuf::from("base.img"));
1935 assert_eq!(create_with_len, None);
1936 }
1937 _ => panic!("Expected File variant inside SqliteDiff"),
1938 }
1939 }
1940 _ => panic!("Expected SqliteDiff variant"),
1941 }
1942
1943 let s = "sqldiff:diff.sqlite:file:base.img";
1945 let disk = DiskCliKind::from_str(s).unwrap();
1946 match disk {
1947 DiskCliKind::SqliteDiff { path, create, disk } => {
1948 assert_eq!(path, PathBuf::from("diff.sqlite"));
1949 assert!(!create);
1950 match *disk {
1951 DiskCliKind::File {
1952 path,
1953 create_with_len,
1954 } => {
1955 assert_eq!(path, PathBuf::from("base.img"));
1956 assert_eq!(create_with_len, None);
1957 }
1958 _ => panic!("Expected File variant inside SqliteDiff"),
1959 }
1960 }
1961 _ => panic!("Expected SqliteDiff variant"),
1962 }
1963 }
1964
1965 #[test]
1966 fn test_parse_autocache_sqlite_disk() {
1967 let disk = with_env_var("OPENVMM_AUTO_CACHE_PATH", "/tmp/cache", || {
1969 DiskCliKind::from_str("autocache::file:disk.vhd").unwrap()
1970 });
1971 assert!(matches!(
1972 disk,
1973 DiskCliKind::AutoCacheSqlite {
1974 cache_path,
1975 key,
1976 disk: _disk,
1977 } if cache_path == "/tmp/cache" && key.is_none()
1978 ));
1979
1980 assert!(DiskCliKind::from_str("autocache::file:disk.vhd").is_err());
1982 }
1983
1984 #[test]
1985 fn test_parse_disk_errors() {
1986 assert!(DiskCliKind::from_str("invalid:").is_err());
1987 assert!(DiskCliKind::from_str("memory:extra").is_err());
1988
1989 assert!(DiskCliKind::from_str("sqlite:").is_err());
1991 }
1992
1993 #[test]
1994 fn test_parse_errors() {
1995 assert!(DiskCliKind::from_str("mem:invalid").is_err());
1997
1998 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
2000
2001 unsafe {
2005 std::env::remove_var("OPENVMM_AUTO_CACHE_PATH");
2006 }
2007 assert!(DiskCliKind::from_str("autocache:key:file:disk.vhd").is_err());
2008
2009 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
2011
2012 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
2014
2015 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
2017
2018 assert!(DiskCliKind::from_str("invalid:path").is_err());
2020
2021 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
2023 }
2024
2025 #[test]
2026 fn test_fs_args_from_str() {
2027 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
2028 assert_eq!(args.tag, "tag1");
2029 assert_eq!(args.path, "/path/to/fs");
2030
2031 assert!(FsArgs::from_str("tag1").is_err());
2033 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
2034 }
2035
2036 #[test]
2037 fn test_fs_args_with_options_from_str() {
2038 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
2039 assert_eq!(args.tag, "tag1");
2040 assert_eq!(args.path, "/path/to/fs");
2041 assert_eq!(args.options, "opt1;opt2");
2042
2043 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
2045 assert_eq!(args.tag, "tag1");
2046 assert_eq!(args.path, "/path/to/fs");
2047 assert_eq!(args.options, "");
2048
2049 assert!(FsArgsWithOptions::from_str("tag1").is_err());
2051 }
2052
2053 #[test]
2054 fn test_serial_config_from_str() {
2055 assert_eq!(
2056 SerialConfigCli::from_str("none").unwrap(),
2057 SerialConfigCli::None
2058 );
2059 assert_eq!(
2060 SerialConfigCli::from_str("console").unwrap(),
2061 SerialConfigCli::Console
2062 );
2063 assert_eq!(
2064 SerialConfigCli::from_str("stderr").unwrap(),
2065 SerialConfigCli::Stderr
2066 );
2067
2068 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
2070 if let SerialConfigCli::File(path) = file_config {
2071 assert_eq!(path.to_str().unwrap(), "/path/to/file");
2072 } else {
2073 panic!("Expected File variant");
2074 }
2075
2076 match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
2078 SerialConfigCli::NewConsole(None, Some(name)) => {
2079 assert_eq!(name, "MyTerm");
2080 }
2081 _ => panic!("Expected NewConsole variant with name"),
2082 }
2083
2084 match SerialConfigCli::from_str("term").unwrap() {
2086 SerialConfigCli::NewConsole(None, None) => (),
2087 _ => panic!("Expected NewConsole variant without name"),
2088 }
2089
2090 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
2092 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
2093 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
2094 assert_eq!(name, "MyTerm");
2095 }
2096 _ => panic!("Expected NewConsole variant with name"),
2097 }
2098
2099 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
2101 SerialConfigCli::NewConsole(Some(path), None) => {
2102 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
2103 }
2104 _ => panic!("Expected NewConsole variant without name"),
2105 }
2106
2107 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
2109 SerialConfigCli::Tcp(addr) => {
2110 assert_eq!(addr.to_string(), "127.0.0.1:1234");
2111 }
2112 _ => panic!("Expected Tcp variant"),
2113 }
2114
2115 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
2117 SerialConfigCli::Pipe(path) => {
2118 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
2119 }
2120 _ => panic!("Expected Pipe variant"),
2121 }
2122
2123 assert!(SerialConfigCli::from_str("").is_err());
2125 assert!(SerialConfigCli::from_str("unknown").is_err());
2126 assert!(SerialConfigCli::from_str("file").is_err());
2127 assert!(SerialConfigCli::from_str("listen").is_err());
2128 }
2129
2130 #[test]
2131 fn test_endpoint_config_from_str() {
2132 assert!(matches!(
2134 EndpointConfigCli::from_str("none").unwrap(),
2135 EndpointConfigCli::None
2136 ));
2137
2138 match EndpointConfigCli::from_str("consomme").unwrap() {
2140 EndpointConfigCli::Consomme { cidr: None } => (),
2141 _ => panic!("Expected Consomme variant without cidr"),
2142 }
2143
2144 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
2146 EndpointConfigCli::Consomme { cidr: Some(cidr) } => {
2147 assert_eq!(cidr, "192.168.0.0/24");
2148 }
2149 _ => panic!("Expected Consomme variant with cidr"),
2150 }
2151
2152 match EndpointConfigCli::from_str("dio").unwrap() {
2154 EndpointConfigCli::Dio { id: None } => (),
2155 _ => panic!("Expected Dio variant without id"),
2156 }
2157
2158 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
2160 EndpointConfigCli::Dio { id: Some(id) } => {
2161 assert_eq!(id, "test_id");
2162 }
2163 _ => panic!("Expected Dio variant with id"),
2164 }
2165
2166 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
2168 EndpointConfigCli::Tap { name } => {
2169 assert_eq!(name, "tap0");
2170 }
2171 _ => panic!("Expected Tap variant"),
2172 }
2173
2174 assert!(EndpointConfigCli::from_str("invalid").is_err());
2176 }
2177
2178 #[test]
2179 fn test_nic_config_from_str() {
2180 use openvmm_defs::config::DeviceVtl;
2181
2182 let config = NicConfigCli::from_str("none").unwrap();
2184 assert_eq!(config.vtl, DeviceVtl::Vtl0);
2185 assert!(config.max_queues.is_none());
2186 assert!(!config.underhill);
2187 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2188
2189 let config = NicConfigCli::from_str("vtl2:none").unwrap();
2191 assert_eq!(config.vtl, DeviceVtl::Vtl2);
2192 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2193
2194 let config = NicConfigCli::from_str("queues=4:none").unwrap();
2196 assert_eq!(config.max_queues, Some(4));
2197 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2198
2199 let config = NicConfigCli::from_str("uh:none").unwrap();
2201 assert!(config.underhill);
2202 assert!(matches!(config.endpoint, EndpointConfigCli::None));
2203
2204 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
2206 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); }
2208
2209 #[test]
2210 fn test_smt_config_from_str() {
2211 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
2212 assert_eq!(
2213 SmtConfigCli::from_str("force").unwrap(),
2214 SmtConfigCli::Force
2215 );
2216 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
2217
2218 assert!(SmtConfigCli::from_str("invalid").is_err());
2220 assert!(SmtConfigCli::from_str("").is_err());
2221 }
2222
2223 #[test]
2224 fn test_pcat_boot_order_from_str() {
2225 let order = PcatBootOrderCli::from_str("optical").unwrap();
2227 assert_eq!(order.0[0], PcatBootDevice::Optical);
2228
2229 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
2231 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
2232 assert_eq!(order.0[1], PcatBootDevice::Network);
2233
2234 assert!(PcatBootOrderCli::from_str("invalid").is_err());
2236 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
2238
2239 #[test]
2240 fn test_floppy_disk_from_str() {
2241 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
2243 assert!(!disk.read_only);
2244 match disk.kind {
2245 DiskCliKind::File {
2246 path,
2247 create_with_len,
2248 } => {
2249 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
2250 assert_eq!(create_with_len, None);
2251 }
2252 _ => panic!("Expected File variant"),
2253 }
2254
2255 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
2257 assert!(disk.read_only);
2258
2259 assert!(FloppyDiskCli::from_str("").is_err());
2261 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
2262 }
2263
2264 #[test]
2265 fn test_pcie_root_complex_from_str() {
2266 const ONE_MB: u64 = 1024 * 1024;
2267 const ONE_GB: u64 = 1024 * ONE_MB;
2268
2269 const DEFAULT_LOW_MMIO: u32 = (4 * ONE_MB) as u32;
2270 const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
2271
2272 assert_eq!(
2273 PcieRootComplexCli::from_str("rc0").unwrap(),
2274 PcieRootComplexCli {
2275 name: "rc0".to_string(),
2276 segment: 0,
2277 start_bus: 0,
2278 end_bus: 255,
2279 low_mmio: DEFAULT_LOW_MMIO,
2280 high_mmio: DEFAULT_HIGH_MMIO,
2281 }
2282 );
2283
2284 assert_eq!(
2285 PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
2286 PcieRootComplexCli {
2287 name: "rc1".to_string(),
2288 segment: 1,
2289 start_bus: 0,
2290 end_bus: 255,
2291 low_mmio: DEFAULT_LOW_MMIO,
2292 high_mmio: DEFAULT_HIGH_MMIO,
2293 }
2294 );
2295
2296 assert_eq!(
2297 PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
2298 PcieRootComplexCli {
2299 name: "rc2".to_string(),
2300 segment: 0,
2301 start_bus: 32,
2302 end_bus: 255,
2303 low_mmio: DEFAULT_LOW_MMIO,
2304 high_mmio: DEFAULT_HIGH_MMIO,
2305 }
2306 );
2307
2308 assert_eq!(
2309 PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
2310 PcieRootComplexCli {
2311 name: "rc3".to_string(),
2312 segment: 0,
2313 start_bus: 0,
2314 end_bus: 31,
2315 low_mmio: DEFAULT_LOW_MMIO,
2316 high_mmio: DEFAULT_HIGH_MMIO,
2317 }
2318 );
2319
2320 assert_eq!(
2321 PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
2322 PcieRootComplexCli {
2323 name: "rc4".to_string(),
2324 segment: 0,
2325 start_bus: 32,
2326 end_bus: 127,
2327 low_mmio: DEFAULT_LOW_MMIO,
2328 high_mmio: 2 * ONE_GB,
2329 }
2330 );
2331
2332 assert_eq!(
2333 PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
2334 PcieRootComplexCli {
2335 name: "rc5".to_string(),
2336 segment: 2,
2337 start_bus: 32,
2338 end_bus: 127,
2339 low_mmio: DEFAULT_LOW_MMIO,
2340 high_mmio: DEFAULT_HIGH_MMIO,
2341 }
2342 );
2343
2344 assert_eq!(
2345 PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
2346 PcieRootComplexCli {
2347 name: "rc6".to_string(),
2348 segment: 0,
2349 start_bus: 0,
2350 end_bus: 255,
2351 low_mmio: ONE_MB as u32,
2352 high_mmio: 64 * ONE_GB,
2353 }
2354 );
2355
2356 assert!(PcieRootComplexCli::from_str("").is_err());
2358 assert!(PcieRootComplexCli::from_str("poorly,").is_err());
2359 assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
2360 assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
2361 assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
2362 assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
2363 assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
2364 assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
2365 assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
2366 assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
2367 assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
2368 }
2369
2370 #[test]
2371 fn test_pcie_root_port_from_str() {
2372 assert_eq!(
2373 PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
2374 PcieRootPortCli {
2375 root_complex_name: "rc0".to_string(),
2376 name: "rc0rp0".to_string(),
2377 hotplug: false,
2378 }
2379 );
2380
2381 assert_eq!(
2382 PcieRootPortCli::from_str("my_rc:port2").unwrap(),
2383 PcieRootPortCli {
2384 root_complex_name: "my_rc".to_string(),
2385 name: "port2".to_string(),
2386 hotplug: false,
2387 }
2388 );
2389
2390 assert_eq!(
2392 PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
2393 PcieRootPortCli {
2394 root_complex_name: "my_rc".to_string(),
2395 name: "port2".to_string(),
2396 hotplug: true,
2397 }
2398 );
2399
2400 assert!(PcieRootPortCli::from_str("").is_err());
2402 assert!(PcieRootPortCli::from_str("rp0").is_err());
2403 assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
2404 assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
2405 assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
2406 }
2407
2408 #[test]
2409 fn test_pcie_switch_from_str() {
2410 assert_eq!(
2411 GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
2412 GenericPcieSwitchCli {
2413 port_name: "rp0".to_string(),
2414 name: "switch0".to_string(),
2415 num_downstream_ports: 4,
2416 hotplug: false,
2417 }
2418 );
2419
2420 assert_eq!(
2421 GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
2422 GenericPcieSwitchCli {
2423 port_name: "port1".to_string(),
2424 name: "my_switch".to_string(),
2425 num_downstream_ports: 4,
2426 hotplug: false,
2427 }
2428 );
2429
2430 assert_eq!(
2431 GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
2432 GenericPcieSwitchCli {
2433 port_name: "rp2".to_string(),
2434 name: "sw".to_string(),
2435 num_downstream_ports: 8,
2436 hotplug: false,
2437 }
2438 );
2439
2440 assert_eq!(
2442 GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
2443 GenericPcieSwitchCli {
2444 port_name: "switch0-downstream-1".to_string(),
2445 name: "child_switch".to_string(),
2446 num_downstream_ports: 4,
2447 hotplug: false,
2448 }
2449 );
2450
2451 assert_eq!(
2453 GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
2454 GenericPcieSwitchCli {
2455 port_name: "rp0".to_string(),
2456 name: "switch0".to_string(),
2457 num_downstream_ports: 4,
2458 hotplug: true,
2459 }
2460 );
2461
2462 assert_eq!(
2464 GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
2465 GenericPcieSwitchCli {
2466 port_name: "rp0".to_string(),
2467 name: "switch0".to_string(),
2468 num_downstream_ports: 8,
2469 hotplug: true,
2470 }
2471 );
2472
2473 assert!(GenericPcieSwitchCli::from_str("").is_err());
2475 assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
2476 assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
2477 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
2478 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
2479 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
2480 assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
2481 }
2482
2483 #[test]
2484 fn test_pcie_remote_from_str() {
2485 assert_eq!(
2487 PcieRemoteCli::from_str("rc0rp0").unwrap(),
2488 PcieRemoteCli {
2489 port_name: "rc0rp0".to_string(),
2490 socket_path: None,
2491 hu: 0,
2492 controller: 0,
2493 }
2494 );
2495
2496 assert_eq!(
2498 PcieRemoteCli::from_str("rc0rp0,socket=/tmp/custom.sock").unwrap(),
2499 PcieRemoteCli {
2500 port_name: "rc0rp0".to_string(),
2501 socket_path: Some("/tmp/custom.sock".to_string()),
2502 hu: 0,
2503 controller: 0,
2504 }
2505 );
2506
2507 assert_eq!(
2509 PcieRemoteCli::from_str("myport,socket=/tmp/dev.sock,hu=1,controller=2").unwrap(),
2510 PcieRemoteCli {
2511 port_name: "myport".to_string(),
2512 socket_path: Some("/tmp/dev.sock".to_string()),
2513 hu: 1,
2514 controller: 2,
2515 }
2516 );
2517
2518 assert_eq!(
2520 PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
2521 PcieRemoteCli {
2522 port_name: "port0".to_string(),
2523 socket_path: None,
2524 hu: 5,
2525 controller: 3,
2526 }
2527 );
2528
2529 assert!(PcieRemoteCli::from_str("").is_err());
2531 assert!(PcieRemoteCli::from_str("port,socket=").is_err());
2532 assert!(PcieRemoteCli::from_str("port,hu=").is_err());
2533 assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
2534 assert!(PcieRemoteCli::from_str("port,controller=").is_err());
2535 assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
2536 assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
2537 }
2538}