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