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