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"#)]
172 #[clap(long)]
173 pub nvme: Vec<DiskCli>,
174
175 #[clap(long, value_name = "COUNT", default_value = "0")]
177 pub scsi_sub_channels: u16,
178
179 #[clap(long)]
181 pub nic: bool,
182
183 #[clap(long)]
188 pub net: Vec<NicConfigCli>,
189
190 #[clap(long, value_name = "SWITCH_ID")]
194 pub kernel_vmnic: Vec<String>,
195
196 #[clap(long)]
198 pub gfx: bool,
199
200 #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
202 pub vtl2_gfx: bool,
203
204 #[clap(long)]
206 pub vnc: bool,
207
208 #[clap(long, value_name = "PORT", default_value = "5900")]
210 pub vnc_port: u16,
211
212 #[cfg(guest_arch = "x86_64")]
214 #[clap(long, default_value_t)]
215 pub apic_id_offset: u32,
216
217 #[clap(long)]
219 pub vps_per_socket: Option<u32>,
220
221 #[clap(long, default_value = "auto")]
223 pub smt: SmtConfigCli,
224
225 #[cfg(guest_arch = "x86_64")]
227 #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
228 pub x2apic: X2ApicConfig,
229
230 #[clap(long)]
232 pub virtio_console: bool,
233
234 #[clap(long, conflicts_with("virtio_console"))]
236 pub virtio_console_pci: bool,
237
238 #[clap(long, value_name = "SERIAL")]
240 pub com1: Option<SerialConfigCli>,
241
242 #[clap(long, value_name = "SERIAL")]
244 pub com2: Option<SerialConfigCli>,
245
246 #[clap(long, value_name = "SERIAL")]
248 pub com3: Option<SerialConfigCli>,
249
250 #[clap(long, value_name = "SERIAL")]
252 pub com4: Option<SerialConfigCli>,
253
254 #[clap(long, value_name = "SERIAL")]
256 pub virtio_serial: Option<SerialConfigCli>,
257
258 #[structopt(long, value_name = "SERIAL")]
260 pub vmbus_com1_serial: Option<SerialConfigCli>,
261
262 #[structopt(long, value_name = "SERIAL")]
264 pub vmbus_com2_serial: Option<SerialConfigCli>,
265
266 #[clap(long, value_name = "SERIAL")]
268 pub debugcon: Option<DebugconSerialConfigCli>,
269
270 #[clap(long, short = 'e')]
272 pub uefi: bool,
273
274 #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
276 pub uefi_firmware: OptionalPathBuf,
277
278 #[clap(long, requires("uefi"))]
280 pub uefi_debug: bool,
281
282 #[clap(long, requires("uefi"))]
284 pub uefi_enable_memory_protections: bool,
285
286 #[clap(long, requires("pcat"))]
297 pub pcat_boot_order: Option<PcatBootOrderCli>,
298
299 #[clap(long, conflicts_with("uefi"))]
301 pub pcat: bool,
302
303 #[clap(long, requires("pcat"), value_name = "FILE")]
305 pub pcat_firmware: Option<PathBuf>,
306
307 #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
309 pub igvm: Option<PathBuf>,
310
311 #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
314 pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
315
316 #[clap(long, value_name = "tag,root_path")]
318 pub virtio_9p: Vec<FsArgs>,
319
320 #[clap(long)]
322 pub virtio_9p_debug: bool,
323
324 #[clap(long, value_name = "tag,root_path,[options]")]
326 pub virtio_fs: Vec<FsArgsWithOptions>,
327
328 #[clap(long, value_name = "tag,root_path")]
330 pub virtio_fs_shmem: Vec<FsArgs>,
331
332 #[clap(long, value_name = "BUS", default_value = "auto")]
334 pub virtio_fs_bus: VirtioBusCli,
335
336 #[clap(long, value_name = "PATH")]
338 pub virtio_pmem: Option<String>,
339
340 #[clap(long)]
346 pub virtio_net: Vec<NicConfigCli>,
347
348 #[clap(long, value_name = "PATH")]
350 pub log_file: Option<PathBuf>,
351
352 #[clap(long, value_name = "SOCKETPATH")]
354 pub ttrpc: Option<PathBuf>,
355
356 #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
358 pub grpc: Option<PathBuf>,
359
360 #[clap(long)]
362 pub single_process: bool,
363
364 #[cfg(windows)]
366 #[clap(long, value_name = "PATH")]
367 pub device: Vec<String>,
368
369 #[clap(long, requires("uefi"))]
371 pub disable_frontpage: bool,
372
373 #[clap(long)]
375 pub tpm: bool,
376
377 #[clap(long, default_value = "control", hide(true))]
381 #[expect(clippy::option_option)]
382 pub internal_worker: Option<Option<String>>,
383
384 #[clap(long, requires("vtl2"))]
386 pub vmbus_redirect: bool,
387
388 #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
390 pub vmbus_max_version: Option<u32>,
391
392 #[clap(long_help = r#"
396e.g: --vmgs memdiff:file:/path/to/file.vmgs
397
398syntax: <path> | kind:<arg>[,flag]
399
400valid disk kinds:
401 `mem:<len>` memory backed disk
402 <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
403 `memdiff:<disk>[;create=<len>]` memory backed diff disk
404 <disk>: lower disk, e.g.: `file:base.img`
405 `file:<path>` file-backed disk
406 <path>: path to file
407
408flags:
409 `fmt` reprovision the VMGS before boot
410 `fmt-on-fail` reprovision the VMGS before boot if it is corrupted
411"#)]
412 #[clap(long)]
413 pub vmgs: Option<VmgsCli>,
414
415 #[clap(long, requires("vmgs"))]
417 pub test_gsp_by_id: bool,
418
419 #[clap(long, requires("pcat"), value_name = "FILE")]
421 pub vga_firmware: Option<PathBuf>,
422
423 #[clap(long)]
425 pub secure_boot: bool,
426
427 #[clap(long)]
429 pub secure_boot_template: Option<SecureBootTemplateCli>,
430
431 #[clap(long, value_name = "PATH")]
433 pub custom_uefi_json: Option<PathBuf>,
434
435 #[clap(long, hide(true))]
440 pub relay_console_path: Option<PathBuf>,
441
442 #[clap(long, hide(true))]
446 pub relay_console_title: Option<String>,
447
448 #[clap(long, value_name = "PORT")]
450 pub gdb: Option<u16>,
451
452 #[clap(long)]
454 pub mana: Vec<NicConfigCli>,
455
456 #[clap(long, value_parser = parse_hypervisor)]
458 pub hypervisor: Option<Hypervisor>,
459
460 #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
468 pub custom_dsdt: Option<PathBuf>,
469
470 #[clap(long_help = r#"
480e.g: --ide memdiff:file:/path/to/disk.vhd
481
482syntax: <path> | kind:<arg>[,flag,opt=arg,...]
483
484valid disk kinds:
485 `mem:<len>` memory backed disk
486 <len>: length of ramdisk, e.g.: `1G`
487 `memdiff:<disk>` memory backed diff disk
488 <disk>: lower disk, e.g.: `file:base.img`
489 `file:<path>` file-backed disk
490 <path>: path to file
491
492flags:
493 `ro` open disk as read-only
494 `s` attach drive to secondary ide channel
495 `dvd` specifies that device is cd/dvd and it is read_only
496"#)]
497 #[clap(long, value_name = "FILE", requires("pcat"))]
498 pub ide: Vec<IdeDiskCli>,
499
500 #[clap(long_help = r#"
503e.g: --floppy memdiff:/path/to/disk.vfd,ro
504
505syntax: <path> | kind:<arg>[,flag,opt=arg,...]
506
507valid disk kinds:
508 `mem:<len>` memory backed disk
509 <len>: length of ramdisk, e.g.: `1G`
510 `memdiff:<disk>` memory backed diff disk
511 <disk>: lower disk, e.g.: `file:base.img`
512 `file:<path>` file-backed disk
513 <path>: path to file
514
515flags:
516 `ro` open disk as read-only
517"#)]
518 #[clap(long, value_name = "FILE", requires("pcat"))]
519 pub floppy: Vec<FloppyDiskCli>,
520
521 #[clap(long)]
523 pub guest_watchdog: bool,
524
525 #[clap(long)]
527 pub openhcl_dump_path: Option<PathBuf>,
528
529 #[clap(long)]
531 pub halt_on_reset: bool,
532
533 #[clap(long)]
535 pub write_saved_state_proto: Option<PathBuf>,
536
537 #[clap(long)]
539 pub imc: Option<PathBuf>,
540
541 #[clap(long)]
543 pub mcr: bool, #[clap(long)]
547 pub battery: bool,
548
549 #[clap(long)]
551 pub uefi_console_mode: Option<UefiConsoleModeCli>,
552
553 #[clap(long)]
555 pub default_boot_always_attempt: bool,
556
557 #[clap(long_help = r#"
559e.g: --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
560
561syntax: <name>[,opt=arg,...]
562
563options:
564 `segment=<value>` configures the PCI Express segment, default 0
565 `start_bus=<value>` lowest valid bus number, default 0
566 `end_bus=<value>` highest valid bus number, default 255
567 `low_mmio=<size>` low MMIO window size, default 4M
568 `high_mmio=<size>` high MMIO window size, default 1G
569"#)]
570 #[clap(long, conflicts_with("pcat"))]
571 pub pcie_root_complex: Vec<PcieRootComplexCli>,
572
573 #[clap(long_help = r#"
575e.g: --pcie-root-port rc0:rc0rp0
576
577syntax: <root_complex_name>:<name>
578"#)]
579 #[clap(long, conflicts_with("pcat"))]
580 pub pcie_root_port: Vec<PcieRootPortCli>,
581}
582
583#[derive(Clone, Debug, PartialEq)]
584pub struct FsArgs {
585 pub tag: String,
586 pub path: String,
587}
588
589impl FromStr for FsArgs {
590 type Err = anyhow::Error;
591
592 fn from_str(s: &str) -> Result<Self, Self::Err> {
593 let mut s = s.split(',');
594 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
595 anyhow::bail!("expected <tag>,<path>");
596 };
597 Ok(Self {
598 tag: tag.to_owned(),
599 path: path.to_owned(),
600 })
601 }
602}
603
604#[derive(Clone, Debug, PartialEq)]
605pub struct FsArgsWithOptions {
606 pub tag: String,
608 pub path: String,
610 pub options: String,
612}
613
614impl FromStr for FsArgsWithOptions {
615 type Err = anyhow::Error;
616
617 fn from_str(s: &str) -> Result<Self, Self::Err> {
618 let mut s = s.split(',');
619 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
620 anyhow::bail!("expected <tag>,<path>[,<options>]");
621 };
622 let options = s.collect::<Vec<_>>().join(";");
623 Ok(Self {
624 tag: tag.to_owned(),
625 path: path.to_owned(),
626 options,
627 })
628 }
629}
630
631#[derive(Copy, Clone, clap::ValueEnum)]
632pub enum VirtioBusCli {
633 Auto,
634 Mmio,
635 Pci,
636 Vpci,
637}
638
639#[derive(clap::ValueEnum, Clone, Copy)]
640pub enum SecureBootTemplateCli {
641 Windows,
642 UefiCa,
643}
644
645fn parse_memory(s: &str) -> anyhow::Result<u64> {
646 if s == "VMGS_DEFAULT" {
647 Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
648 } else {
649 || -> Option<u64> {
650 let mut b = s.as_bytes();
651 if s.ends_with('B') {
652 b = &b[..b.len() - 1]
653 }
654 if b.is_empty() {
655 return None;
656 }
657 let multi = match b[b.len() - 1] as char {
658 'T' => Some(1024 * 1024 * 1024 * 1024),
659 'G' => Some(1024 * 1024 * 1024),
660 'M' => Some(1024 * 1024),
661 'K' => Some(1024),
662 _ => None,
663 };
664 if multi.is_some() {
665 b = &b[..b.len() - 1]
666 }
667 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
668 Some(n * multi.unwrap_or(1))
669 }()
670 .with_context(|| format!("invalid memory size '{0}'", s))
671 }
672}
673
674fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
676 match s.strip_prefix("0x") {
677 Some(rest) => u64::from_str_radix(rest, 16),
678 None => s.parse::<u64>(),
679 }
680}
681
682#[derive(Clone, Debug, PartialEq)]
683pub enum DiskCliKind {
684 Memory(u64),
686 MemoryDiff(Box<DiskCliKind>),
688 Sqlite {
690 path: PathBuf,
691 create_with_len: Option<u64>,
692 },
693 SqliteDiff {
695 path: PathBuf,
696 create: bool,
697 disk: Box<DiskCliKind>,
698 },
699 AutoCacheSqlite {
701 cache_path: String,
702 key: Option<String>,
703 disk: Box<DiskCliKind>,
704 },
705 PersistentReservationsWrapper(Box<DiskCliKind>),
707 File {
709 path: PathBuf,
710 create_with_len: Option<u64>,
711 },
712 Blob {
714 kind: BlobKind,
715 url: String,
716 },
717 Crypt {
719 cipher: DiskCipher,
720 key_file: PathBuf,
721 disk: Box<DiskCliKind>,
722 },
723 DelayDiskWrapper {
725 delay_ms: u64,
726 disk: Box<DiskCliKind>,
727 },
728}
729
730#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
731pub enum DiskCipher {
732 #[clap(name = "xts-aes-256")]
733 XtsAes256,
734}
735
736#[derive(Copy, Clone, Debug, PartialEq)]
737pub enum BlobKind {
738 Flat,
739 Vhd1,
740}
741
742fn parse_path_and_len(arg: &str) -> anyhow::Result<(PathBuf, Option<u64>)> {
743 Ok(match arg.split_once(';') {
744 Some((path, len)) => {
745 let Some(len) = len.strip_prefix("create=") else {
746 anyhow::bail!("invalid syntax after ';', expected 'create=<len>'")
747 };
748
749 let len = parse_memory(len)?;
750
751 (path.into(), Some(len))
752 }
753 None => (arg.into(), None),
754 })
755}
756
757impl FromStr for DiskCliKind {
758 type Err = anyhow::Error;
759
760 fn from_str(s: &str) -> anyhow::Result<Self> {
761 let disk = match s.split_once(':') {
762 None => {
764 let (path, create_with_len) = parse_path_and_len(s)?;
765 DiskCliKind::File {
766 path,
767 create_with_len,
768 }
769 }
770 Some((kind, arg)) => match kind {
771 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
772 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
773 "sql" => {
774 let (path, create_with_len) = parse_path_and_len(arg)?;
775 DiskCliKind::Sqlite {
776 path,
777 create_with_len,
778 }
779 }
780 "sqldiff" => {
781 let (path_and_opts, kind) =
782 arg.split_once(':').context("expected path[;opts]:kind")?;
783 let disk = Box::new(kind.parse()?);
784 match path_and_opts.split_once(';') {
785 Some((path, create)) => {
786 if create != "create" {
787 anyhow::bail!("invalid syntax after ';', expected 'create'")
788 }
789 DiskCliKind::SqliteDiff {
790 path: path.into(),
791 create: true,
792 disk,
793 }
794 }
795 None => DiskCliKind::SqliteDiff {
796 path: path_and_opts.into(),
797 create: false,
798 disk,
799 },
800 }
801 }
802 "autocache" => {
803 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
804 let cache_path = std::env::var("OPENVMM_AUTO_CACHE_PATH")
805 .context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
806 DiskCliKind::AutoCacheSqlite {
807 cache_path,
808 key: (!key.is_empty()).then(|| key.to_string()),
809 disk: Box::new(kind.parse()?),
810 }
811 }
812 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
813 "file" => {
814 let (path, create_with_len) = parse_path_and_len(arg)?;
815 DiskCliKind::File {
816 path,
817 create_with_len,
818 }
819 }
820 "blob" => {
821 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
822 let blob_kind = match blob_kind {
823 "flat" => BlobKind::Flat,
824 "vhd1" => BlobKind::Vhd1,
825 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
826 };
827 DiskCliKind::Blob {
828 kind: blob_kind,
829 url: url.to_string(),
830 }
831 }
832 "crypt" => {
833 let (cipher, (key, kind)) = arg
834 .split_once(':')
835 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
836 .context("expected cipher:key_file:kind")?;
837 DiskCliKind::Crypt {
838 cipher: ValueEnum::from_str(cipher, false)
839 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
840 key_file: PathBuf::from(key),
841 disk: Box::new(kind.parse()?),
842 }
843 }
844 kind => {
845 let (path, create_with_len) = parse_path_and_len(s)?;
850 if path.has_root() {
851 DiskCliKind::File {
852 path,
853 create_with_len,
854 }
855 } else {
856 anyhow::bail!("invalid disk kind {kind}");
857 }
858 }
859 },
860 };
861 Ok(disk)
862 }
863}
864
865#[derive(Clone)]
866pub struct VmgsCli {
867 pub kind: DiskCliKind,
868 pub provision: ProvisionVmgs,
869}
870
871#[derive(Copy, Clone)]
872pub enum ProvisionVmgs {
873 OnEmpty,
874 OnFailure,
875 True,
876}
877
878impl FromStr for VmgsCli {
879 type Err = anyhow::Error;
880
881 fn from_str(s: &str) -> anyhow::Result<Self> {
882 let (kind, opt) = s
883 .split_once(',')
884 .map(|(k, o)| (k, Some(o)))
885 .unwrap_or((s, None));
886 let kind = kind.parse()?;
887
888 let provision = match opt {
889 None => ProvisionVmgs::OnEmpty,
890 Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
891 Some("fmt") => ProvisionVmgs::True,
892 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
893 };
894
895 Ok(VmgsCli { kind, provision })
896 }
897}
898
899#[derive(Clone)]
901pub struct DiskCli {
902 pub vtl: DeviceVtl,
903 pub kind: DiskCliKind,
904 pub read_only: bool,
905 pub is_dvd: bool,
906 pub underhill: Option<UnderhillDiskSource>,
907}
908
909#[derive(Copy, Clone)]
910pub enum UnderhillDiskSource {
911 Scsi,
912 Nvme,
913}
914
915impl FromStr for DiskCli {
916 type Err = anyhow::Error;
917
918 fn from_str(s: &str) -> anyhow::Result<Self> {
919 let mut opts = s.split(',');
920 let kind = opts.next().unwrap().parse()?;
921
922 let mut read_only = false;
923 let mut is_dvd = false;
924 let mut underhill = None;
925 let mut vtl = DeviceVtl::Vtl0;
926 for opt in opts {
927 let mut s = opt.split('=');
928 let opt = s.next().unwrap();
929 match opt {
930 "ro" => read_only = true,
931 "dvd" => {
932 is_dvd = true;
933 read_only = true;
934 }
935 "vtl2" => {
936 vtl = DeviceVtl::Vtl2;
937 }
938 "uh" => underhill = Some(UnderhillDiskSource::Scsi),
939 "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
940 opt => anyhow::bail!("unknown option: '{opt}'"),
941 }
942 }
943
944 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
945 anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
946 }
947
948 Ok(DiskCli {
949 vtl,
950 kind,
951 read_only,
952 is_dvd,
953 underhill,
954 })
955 }
956}
957
958#[derive(Clone)]
960pub struct IdeDiskCli {
961 pub kind: DiskCliKind,
962 pub read_only: bool,
963 pub channel: Option<u8>,
964 pub device: Option<u8>,
965 pub is_dvd: bool,
966}
967
968impl FromStr for IdeDiskCli {
969 type Err = anyhow::Error;
970
971 fn from_str(s: &str) -> anyhow::Result<Self> {
972 let mut opts = s.split(',');
973 let kind = opts.next().unwrap().parse()?;
974
975 let mut read_only = false;
976 let mut channel = None;
977 let mut device = None;
978 let mut is_dvd = false;
979 for opt in opts {
980 let mut s = opt.split('=');
981 let opt = s.next().unwrap();
982 match opt {
983 "ro" => read_only = true,
984 "p" => channel = Some(0),
985 "s" => channel = Some(1),
986 "0" => device = Some(0),
987 "1" => device = Some(1),
988 "dvd" => {
989 is_dvd = true;
990 read_only = true;
991 }
992 _ => anyhow::bail!("unknown option: '{opt}'"),
993 }
994 }
995
996 Ok(IdeDiskCli {
997 kind,
998 read_only,
999 channel,
1000 device,
1001 is_dvd,
1002 })
1003 }
1004}
1005
1006#[derive(Clone, Debug, PartialEq)]
1008pub struct FloppyDiskCli {
1009 pub kind: DiskCliKind,
1010 pub read_only: bool,
1011}
1012
1013impl FromStr for FloppyDiskCli {
1014 type Err = anyhow::Error;
1015
1016 fn from_str(s: &str) -> anyhow::Result<Self> {
1017 if s.is_empty() {
1018 anyhow::bail!("empty disk spec");
1019 }
1020 let mut opts = s.split(',');
1021 let kind = opts.next().unwrap().parse()?;
1022
1023 let mut read_only = false;
1024 for opt in opts {
1025 let mut s = opt.split('=');
1026 let opt = s.next().unwrap();
1027 match opt {
1028 "ro" => read_only = true,
1029 _ => anyhow::bail!("unknown option: '{opt}'"),
1030 }
1031 }
1032
1033 Ok(FloppyDiskCli { kind, read_only })
1034 }
1035}
1036
1037#[derive(Clone)]
1038pub struct DebugconSerialConfigCli {
1039 pub port: u16,
1040 pub serial: SerialConfigCli,
1041}
1042
1043impl FromStr for DebugconSerialConfigCli {
1044 type Err = String;
1045
1046 fn from_str(s: &str) -> Result<Self, Self::Err> {
1047 let Some((port, serial)) = s.split_once(',') else {
1048 return Err("invalid format (missing comma between port and serial)".into());
1049 };
1050
1051 let port: u16 = parse_number(port)
1052 .map_err(|_| "could not parse port".to_owned())?
1053 .try_into()
1054 .map_err(|_| "port must be 16-bit")?;
1055 let serial: SerialConfigCli = serial.parse()?;
1056
1057 Ok(Self { port, serial })
1058 }
1059}
1060
1061#[derive(Clone, Debug, PartialEq)]
1063pub enum SerialConfigCli {
1064 None,
1065 Console,
1066 NewConsole(Option<PathBuf>, Option<String>),
1067 Stderr,
1068 Pipe(PathBuf),
1069 Tcp(SocketAddr),
1070 File(PathBuf),
1071}
1072
1073impl FromStr for SerialConfigCli {
1074 type Err = String;
1075
1076 fn from_str(s: &str) -> Result<Self, Self::Err> {
1077 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1078
1079 let first_key = match keyvalues.first() {
1080 Some(first_pair) => first_pair.0.as_str(),
1081 None => Err("invalid serial configuration: no values supplied")?,
1082 };
1083 let first_value = keyvalues.first().unwrap().1.as_ref();
1084
1085 let ret = match first_key {
1086 "none" => SerialConfigCli::None,
1087 "console" => SerialConfigCli::Console,
1088 "stderr" => SerialConfigCli::Stderr,
1089 "file" => match first_value {
1090 Some(path) => SerialConfigCli::File(path.into()),
1091 None => Err("invalid serial configuration: file requires a value")?,
1092 },
1093 "term" => match first_value {
1094 Some(path) => {
1095 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1097 let window_name = match window_name {
1098 Some((_, Some(name))) => Some(name.clone()),
1099 _ => None,
1100 };
1101
1102 SerialConfigCli::NewConsole(Some(path.into()), window_name)
1103 }
1104 None => SerialConfigCli::NewConsole(None, None),
1105 },
1106 "listen" => match first_value {
1107 Some(path) => {
1108 if let Some(tcp) = path.strip_prefix("tcp:") {
1109 let addr = tcp
1110 .parse()
1111 .map_err(|err| format!("invalid tcp address: {err}"))?;
1112 SerialConfigCli::Tcp(addr)
1113 } else {
1114 SerialConfigCli::Pipe(path.into())
1115 }
1116 }
1117 None => Err(
1118 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1119 )?,
1120 },
1121 _ => {
1122 return Err(format!(
1123 "invalid serial configuration: '{}' is not a known option",
1124 first_key
1125 ));
1126 }
1127 };
1128
1129 Ok(ret)
1130 }
1131}
1132
1133impl SerialConfigCli {
1134 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1137 let mut ret = Vec::new();
1138
1139 for item in s.split(',') {
1141 let mut eqsplit = item.split('=');
1144 let key = eqsplit.next();
1145 let value = eqsplit.next();
1146
1147 if let Some(key) = key {
1148 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1149 } else {
1150 return Err("invalid key=value pair in serial config".into());
1152 }
1153 }
1154 Ok(ret)
1155 }
1156}
1157
1158#[derive(Clone, Debug, PartialEq)]
1159pub enum EndpointConfigCli {
1160 None,
1161 Consomme { cidr: Option<String> },
1162 Dio { id: Option<String> },
1163 Tap { name: String },
1164}
1165
1166impl FromStr for EndpointConfigCli {
1167 type Err = String;
1168
1169 fn from_str(s: &str) -> Result<Self, Self::Err> {
1170 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1171 ["none"] => EndpointConfigCli::None,
1172 ["consomme", s @ ..] => EndpointConfigCli::Consomme {
1173 cidr: s.first().map(|&s| s.to_owned()),
1174 },
1175 ["dio", s @ ..] => EndpointConfigCli::Dio {
1176 id: s.first().map(|s| (*s).to_owned()),
1177 },
1178 ["tap", name] => EndpointConfigCli::Tap {
1179 name: (*name).to_owned(),
1180 },
1181 _ => return Err("invalid network backend".into()),
1182 };
1183
1184 Ok(ret)
1185 }
1186}
1187
1188#[derive(Clone, Debug, PartialEq)]
1189pub struct NicConfigCli {
1190 pub vtl: DeviceVtl,
1191 pub endpoint: EndpointConfigCli,
1192 pub max_queues: Option<u16>,
1193 pub underhill: bool,
1194}
1195
1196impl FromStr for NicConfigCli {
1197 type Err = String;
1198
1199 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1200 let mut vtl = DeviceVtl::Vtl0;
1201 let mut max_queues = None;
1202 let mut underhill = false;
1203 while let Some((opt, rest)) = s.split_once(':') {
1204 if let Some((opt, val)) = opt.split_once('=') {
1205 match opt {
1206 "queues" => {
1207 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1208 }
1209 _ => break,
1210 }
1211 } else {
1212 match opt {
1213 "vtl2" => {
1214 vtl = DeviceVtl::Vtl2;
1215 }
1216 "uh" => underhill = true,
1217 _ => break,
1218 }
1219 }
1220 s = rest;
1221 }
1222
1223 if underhill && vtl != DeviceVtl::Vtl0 {
1224 return Err("`uh` is incompatible with `vtl2`".into());
1225 }
1226
1227 let endpoint = s.parse()?;
1228 Ok(NicConfigCli {
1229 vtl,
1230 endpoint,
1231 max_queues,
1232 underhill,
1233 })
1234 }
1235}
1236
1237#[derive(Debug, Error)]
1238#[error("unknown hypervisor: {0}")]
1239pub struct UnknownHypervisor(String);
1240
1241fn parse_hypervisor(s: &str) -> Result<Hypervisor, UnknownHypervisor> {
1242 match s {
1243 "kvm" => Ok(Hypervisor::Kvm),
1244 "mshv" => Ok(Hypervisor::MsHv),
1245 "whp" => Ok(Hypervisor::Whp),
1246 _ => Err(UnknownHypervisor(s.to_owned())),
1247 }
1248}
1249
1250#[derive(Debug, Error)]
1251#[error("unknown VTL2 relocation type: {0}")]
1252pub struct UnknownVtl2RelocationType(String);
1253
1254fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1255 match s {
1256 "disable" => Ok(Vtl2BaseAddressType::File),
1257 s if s.starts_with("auto=") => {
1258 let s = s.strip_prefix("auto=").unwrap_or_default();
1259 let size = if s == "filesize" {
1260 None
1261 } else {
1262 let size = parse_memory(s).map_err(|e| {
1263 UnknownVtl2RelocationType(format!(
1264 "unable to parse memory size from {} for 'auto=' type, {e}",
1265 e
1266 ))
1267 })?;
1268 Some(size)
1269 };
1270 Ok(Vtl2BaseAddressType::MemoryLayout { size })
1271 }
1272 s if s.starts_with("absolute=") => {
1273 let s = s.strip_prefix("absolute=");
1274 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
1275 UnknownVtl2RelocationType(format!(
1276 "unable to parse number from {} for 'absolute=' type",
1277 e
1278 ))
1279 })?;
1280 Ok(Vtl2BaseAddressType::Absolute(addr))
1281 }
1282 s if s.starts_with("vtl2=") => {
1283 let s = s.strip_prefix("vtl2=").unwrap_or_default();
1284 let size = if s == "filesize" {
1285 None
1286 } else {
1287 let size = parse_memory(s).map_err(|e| {
1288 UnknownVtl2RelocationType(format!(
1289 "unable to parse memory size from {} for 'vtl2=' type, {e}",
1290 e
1291 ))
1292 })?;
1293 Some(size)
1294 };
1295 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
1296 }
1297 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
1298 }
1299}
1300
1301#[derive(Debug, Copy, Clone, PartialEq)]
1302pub enum SmtConfigCli {
1303 Auto,
1304 Force,
1305 Off,
1306}
1307
1308#[derive(Debug, Error)]
1309#[error("expected auto, force, or off")]
1310pub struct BadSmtConfig;
1311
1312impl FromStr for SmtConfigCli {
1313 type Err = BadSmtConfig;
1314
1315 fn from_str(s: &str) -> Result<Self, Self::Err> {
1316 let r = match s {
1317 "auto" => Self::Auto,
1318 "force" => Self::Force,
1319 "off" => Self::Off,
1320 _ => return Err(BadSmtConfig),
1321 };
1322 Ok(r)
1323 }
1324}
1325
1326#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
1327fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
1328 let r = match s {
1329 "auto" => X2ApicConfig::Auto,
1330 "supported" => X2ApicConfig::Supported,
1331 "off" => X2ApicConfig::Unsupported,
1332 "on" => X2ApicConfig::Enabled,
1333 _ => return Err("expected auto, supported, off, or on"),
1334 };
1335 Ok(r)
1336}
1337
1338#[derive(Debug, Copy, Clone, ValueEnum)]
1339pub enum Vtl0LateMapPolicyCli {
1340 Off,
1341 Log,
1342 Halt,
1343 Exception,
1344}
1345
1346#[derive(Debug, Copy, Clone, ValueEnum)]
1347pub enum IsolationCli {
1348 Vbs,
1349}
1350
1351#[derive(Debug, Copy, Clone, PartialEq)]
1352pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
1353
1354impl FromStr for PcatBootOrderCli {
1355 type Err = &'static str;
1356
1357 fn from_str(s: &str) -> Result<Self, Self::Err> {
1358 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
1359 let mut order = Vec::new();
1360
1361 for item in s.split(',') {
1362 let device = match item {
1363 "optical" => PcatBootDevice::Optical,
1364 "hdd" => PcatBootDevice::HardDrive,
1365 "net" => PcatBootDevice::Network,
1366 "floppy" => PcatBootDevice::Floppy,
1367 _ => return Err("unknown boot device type"),
1368 };
1369
1370 let default_pos = default_order
1371 .iter()
1372 .position(|x| x == &Some(device))
1373 .ok_or("cannot pass duplicate boot devices")?;
1374
1375 order.push(default_order[default_pos].take().unwrap());
1376 }
1377
1378 order.extend(default_order.into_iter().flatten());
1379 assert_eq!(order.len(), 4);
1380
1381 Ok(Self(order.try_into().unwrap()))
1382 }
1383}
1384
1385#[derive(Copy, Clone, Debug, ValueEnum)]
1386pub enum UefiConsoleModeCli {
1387 Default,
1388 Com1,
1389 Com2,
1390 None,
1391}
1392
1393#[derive(Clone, Debug, PartialEq)]
1394pub struct PcieRootComplexCli {
1395 pub name: String,
1396 pub segment: u16,
1397 pub start_bus: u8,
1398 pub end_bus: u8,
1399 pub low_mmio: u32,
1400 pub high_mmio: u64,
1401}
1402
1403impl FromStr for PcieRootComplexCli {
1404 type Err = anyhow::Error;
1405
1406 fn from_str(s: &str) -> Result<Self, Self::Err> {
1407 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(',');
1411 let name = opts.next().context("expected root complex name")?;
1412 if name.is_empty() {
1413 anyhow::bail!("must provide a root complex name");
1414 }
1415
1416 let mut segment = 0;
1417 let mut start_bus = 0;
1418 let mut end_bus = 255;
1419 let mut low_mmio = DEFAULT_PCIE_CRS_LOW_SIZE;
1420 let mut high_mmio = DEFAULT_PCIE_CRS_HIGH_SIZE;
1421 for opt in opts {
1422 let mut s = opt.split('=');
1423 let opt = s.next().context("expected option")?;
1424 match opt {
1425 "segment" => {
1426 let seg_str = s.next().context("expected segment number")?;
1427 segment = u16::from_str(seg_str).context("failed to parse segment number")?;
1428 }
1429 "start_bus" => {
1430 let bus_str = s.next().context("expected start bus number")?;
1431 start_bus =
1432 u8::from_str(bus_str).context("failed to parse start bus number")?;
1433 }
1434 "end_bus" => {
1435 let bus_str = s.next().context("expected end bus number")?;
1436 end_bus = u8::from_str(bus_str).context("failed to parse end bus number")?;
1437 }
1438 "low_mmio" => {
1439 let low_mmio_str = s.next().context("expected low MMIO size")?;
1440 low_mmio = parse_memory(low_mmio_str)
1441 .context("failed to parse low MMIO size")?
1442 .try_into()?;
1443 }
1444 "high_mmio" => {
1445 let high_mmio_str = s.next().context("expected high MMIO size")?;
1446 high_mmio =
1447 parse_memory(high_mmio_str).context("failed to parse high MMIO size")?;
1448 }
1449 opt => anyhow::bail!("unknown option: '{opt}'"),
1450 }
1451 }
1452
1453 if start_bus >= end_bus {
1454 anyhow::bail!("start_bus must be less than or equal to end_bus");
1455 }
1456
1457 Ok(PcieRootComplexCli {
1458 name: name.to_string(),
1459 segment,
1460 start_bus,
1461 end_bus,
1462 low_mmio,
1463 high_mmio,
1464 })
1465 }
1466}
1467
1468#[derive(Clone, Debug, PartialEq)]
1469pub struct PcieRootPortCli {
1470 pub root_complex_name: String,
1471 pub name: String,
1472}
1473
1474impl FromStr for PcieRootPortCli {
1475 type Err = anyhow::Error;
1476
1477 fn from_str(s: &str) -> Result<Self, Self::Err> {
1478 let mut opts = s.split(',');
1479 let names = opts.next().context("expected root port identifiers")?;
1480 if names.is_empty() {
1481 anyhow::bail!("must provide root port identifiers");
1482 }
1483
1484 let mut s = names.split(':');
1485 let rc_name = s.next().context("expected name of parent root complex")?;
1486 let rp_name = s.next().context("expected root port name")?;
1487
1488 if let Some(extra) = s.next() {
1489 anyhow::bail!("unexpected token: '{extra}'")
1490 }
1491
1492 if let Some(extra) = opts.next() {
1493 anyhow::bail!("unexpected token: '{extra}'")
1494 }
1495
1496 Ok(PcieRootPortCli {
1497 root_complex_name: rc_name.to_string(),
1498 name: rp_name.to_string(),
1499 })
1500 }
1501}
1502
1503fn default_value_from_arch_env(name: &str) -> OsString {
1511 let prefix = if cfg!(guest_arch = "x86_64") {
1512 "X86_64"
1513 } else if cfg!(guest_arch = "aarch64") {
1514 "AARCH64"
1515 } else {
1516 return Default::default();
1517 };
1518 let prefixed = format!("{}_{}", prefix, name);
1519 std::env::var_os(name)
1520 .or_else(|| std::env::var_os(prefixed))
1521 .unwrap_or_default()
1522}
1523
1524#[derive(Clone)]
1526pub struct OptionalPathBuf(pub Option<PathBuf>);
1527
1528impl From<&std::ffi::OsStr> for OptionalPathBuf {
1529 fn from(s: &std::ffi::OsStr) -> Self {
1530 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
1531 }
1532}
1533
1534#[cfg(test)]
1535#[expect(unsafe_code)]
1537mod tests {
1538 use super::*;
1539
1540 fn with_env_var<F, R>(name: &str, value: &str, f: F) -> R
1541 where
1542 F: FnOnce() -> R,
1543 {
1544 unsafe {
1547 std::env::set_var(name, value);
1548 }
1549 let result = f();
1550 unsafe {
1553 std::env::remove_var(name);
1554 }
1555 result
1556 }
1557
1558 #[test]
1559 fn test_parse_file_disk_with_create() {
1560 let s = "file:test.vhd;create=1G";
1561 let disk = DiskCliKind::from_str(s).unwrap();
1562
1563 match disk {
1564 DiskCliKind::File {
1565 path,
1566 create_with_len,
1567 } => {
1568 assert_eq!(path, PathBuf::from("test.vhd"));
1569 assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); }
1571 _ => panic!("Expected File variant"),
1572 }
1573 }
1574
1575 #[test]
1576 fn test_parse_direct_file_with_create() {
1577 let s = "test.vhd;create=1G";
1578 let disk = DiskCliKind::from_str(s).unwrap();
1579
1580 match disk {
1581 DiskCliKind::File {
1582 path,
1583 create_with_len,
1584 } => {
1585 assert_eq!(path, PathBuf::from("test.vhd"));
1586 assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); }
1588 _ => panic!("Expected File variant"),
1589 }
1590 }
1591
1592 #[test]
1593 fn test_parse_memory_disk() {
1594 let s = "mem:1G";
1595 let disk = DiskCliKind::from_str(s).unwrap();
1596 match disk {
1597 DiskCliKind::Memory(size) => {
1598 assert_eq!(size, 1024 * 1024 * 1024); }
1600 _ => panic!("Expected Memory variant"),
1601 }
1602 }
1603
1604 #[test]
1605 fn test_parse_memory_diff_disk() {
1606 let s = "memdiff:file:base.img";
1607 let disk = DiskCliKind::from_str(s).unwrap();
1608 match disk {
1609 DiskCliKind::MemoryDiff(inner) => match *inner {
1610 DiskCliKind::File {
1611 path,
1612 create_with_len,
1613 } => {
1614 assert_eq!(path, PathBuf::from("base.img"));
1615 assert_eq!(create_with_len, None);
1616 }
1617 _ => panic!("Expected File variant inside MemoryDiff"),
1618 },
1619 _ => panic!("Expected MemoryDiff variant"),
1620 }
1621 }
1622
1623 #[test]
1624 fn test_parse_sqlite_disk() {
1625 let s = "sql:db.sqlite;create=2G";
1626 let disk = DiskCliKind::from_str(s).unwrap();
1627 match disk {
1628 DiskCliKind::Sqlite {
1629 path,
1630 create_with_len,
1631 } => {
1632 assert_eq!(path, PathBuf::from("db.sqlite"));
1633 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
1634 }
1635 _ => panic!("Expected Sqlite variant"),
1636 }
1637
1638 let s = "sql:db.sqlite";
1640 let disk = DiskCliKind::from_str(s).unwrap();
1641 match disk {
1642 DiskCliKind::Sqlite {
1643 path,
1644 create_with_len,
1645 } => {
1646 assert_eq!(path, PathBuf::from("db.sqlite"));
1647 assert_eq!(create_with_len, None);
1648 }
1649 _ => panic!("Expected Sqlite variant"),
1650 }
1651 }
1652
1653 #[test]
1654 fn test_parse_sqlite_diff_disk() {
1655 let s = "sqldiff:diff.sqlite;create:file:base.img";
1657 let disk = DiskCliKind::from_str(s).unwrap();
1658 match disk {
1659 DiskCliKind::SqliteDiff { path, create, disk } => {
1660 assert_eq!(path, PathBuf::from("diff.sqlite"));
1661 assert!(create);
1662 match *disk {
1663 DiskCliKind::File {
1664 path,
1665 create_with_len,
1666 } => {
1667 assert_eq!(path, PathBuf::from("base.img"));
1668 assert_eq!(create_with_len, None);
1669 }
1670 _ => panic!("Expected File variant inside SqliteDiff"),
1671 }
1672 }
1673 _ => panic!("Expected SqliteDiff variant"),
1674 }
1675
1676 let s = "sqldiff:diff.sqlite:file:base.img";
1678 let disk = DiskCliKind::from_str(s).unwrap();
1679 match disk {
1680 DiskCliKind::SqliteDiff { path, create, disk } => {
1681 assert_eq!(path, PathBuf::from("diff.sqlite"));
1682 assert!(!create);
1683 match *disk {
1684 DiskCliKind::File {
1685 path,
1686 create_with_len,
1687 } => {
1688 assert_eq!(path, PathBuf::from("base.img"));
1689 assert_eq!(create_with_len, None);
1690 }
1691 _ => panic!("Expected File variant inside SqliteDiff"),
1692 }
1693 }
1694 _ => panic!("Expected SqliteDiff variant"),
1695 }
1696 }
1697
1698 #[test]
1699 fn test_parse_autocache_sqlite_disk() {
1700 let disk = with_env_var("OPENVMM_AUTO_CACHE_PATH", "/tmp/cache", || {
1702 DiskCliKind::from_str("autocache::file:disk.vhd").unwrap()
1703 });
1704 assert!(matches!(
1705 disk,
1706 DiskCliKind::AutoCacheSqlite {
1707 cache_path,
1708 key,
1709 disk: _disk,
1710 } if cache_path == "/tmp/cache" && key.is_none()
1711 ));
1712
1713 assert!(DiskCliKind::from_str("autocache::file:disk.vhd").is_err());
1715 }
1716
1717 #[test]
1718 fn test_parse_disk_errors() {
1719 assert!(DiskCliKind::from_str("invalid:").is_err());
1720 assert!(DiskCliKind::from_str("memory:extra").is_err());
1721
1722 assert!(DiskCliKind::from_str("sqlite:").is_err());
1724 }
1725
1726 #[test]
1727 fn test_parse_errors() {
1728 assert!(DiskCliKind::from_str("mem:invalid").is_err());
1730
1731 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
1733
1734 unsafe {
1738 std::env::remove_var("OPENVMM_AUTO_CACHE_PATH");
1739 }
1740 assert!(DiskCliKind::from_str("autocache:key:file:disk.vhd").is_err());
1741
1742 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
1744
1745 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
1747
1748 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
1750
1751 assert!(DiskCliKind::from_str("invalid:path").is_err());
1753
1754 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
1756 }
1757
1758 #[test]
1759 fn test_fs_args_from_str() {
1760 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
1761 assert_eq!(args.tag, "tag1");
1762 assert_eq!(args.path, "/path/to/fs");
1763
1764 assert!(FsArgs::from_str("tag1").is_err());
1766 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
1767 }
1768
1769 #[test]
1770 fn test_fs_args_with_options_from_str() {
1771 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
1772 assert_eq!(args.tag, "tag1");
1773 assert_eq!(args.path, "/path/to/fs");
1774 assert_eq!(args.options, "opt1;opt2");
1775
1776 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
1778 assert_eq!(args.tag, "tag1");
1779 assert_eq!(args.path, "/path/to/fs");
1780 assert_eq!(args.options, "");
1781
1782 assert!(FsArgsWithOptions::from_str("tag1").is_err());
1784 }
1785
1786 #[test]
1787 fn test_serial_config_from_str() {
1788 assert_eq!(
1789 SerialConfigCli::from_str("none").unwrap(),
1790 SerialConfigCli::None
1791 );
1792 assert_eq!(
1793 SerialConfigCli::from_str("console").unwrap(),
1794 SerialConfigCli::Console
1795 );
1796 assert_eq!(
1797 SerialConfigCli::from_str("stderr").unwrap(),
1798 SerialConfigCli::Stderr
1799 );
1800
1801 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
1803 if let SerialConfigCli::File(path) = file_config {
1804 assert_eq!(path.to_str().unwrap(), "/path/to/file");
1805 } else {
1806 panic!("Expected File variant");
1807 }
1808
1809 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
1811 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
1812 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
1813 assert_eq!(name, "MyTerm");
1814 }
1815 _ => panic!("Expected NewConsole variant with name"),
1816 }
1817
1818 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
1820 SerialConfigCli::NewConsole(Some(path), None) => {
1821 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
1822 }
1823 _ => panic!("Expected NewConsole variant without name"),
1824 }
1825
1826 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
1828 SerialConfigCli::Tcp(addr) => {
1829 assert_eq!(addr.to_string(), "127.0.0.1:1234");
1830 }
1831 _ => panic!("Expected Tcp variant"),
1832 }
1833
1834 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
1836 SerialConfigCli::Pipe(path) => {
1837 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
1838 }
1839 _ => panic!("Expected Pipe variant"),
1840 }
1841
1842 assert!(SerialConfigCli::from_str("").is_err());
1844 assert!(SerialConfigCli::from_str("unknown").is_err());
1845 assert!(SerialConfigCli::from_str("file").is_err());
1846 assert!(SerialConfigCli::from_str("listen").is_err());
1847 }
1848
1849 #[test]
1850 fn test_endpoint_config_from_str() {
1851 assert!(matches!(
1853 EndpointConfigCli::from_str("none").unwrap(),
1854 EndpointConfigCli::None
1855 ));
1856
1857 match EndpointConfigCli::from_str("consomme").unwrap() {
1859 EndpointConfigCli::Consomme { cidr: None } => (),
1860 _ => panic!("Expected Consomme variant without cidr"),
1861 }
1862
1863 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
1865 EndpointConfigCli::Consomme { cidr: Some(cidr) } => {
1866 assert_eq!(cidr, "192.168.0.0/24");
1867 }
1868 _ => panic!("Expected Consomme variant with cidr"),
1869 }
1870
1871 match EndpointConfigCli::from_str("dio").unwrap() {
1873 EndpointConfigCli::Dio { id: None } => (),
1874 _ => panic!("Expected Dio variant without id"),
1875 }
1876
1877 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
1879 EndpointConfigCli::Dio { id: Some(id) } => {
1880 assert_eq!(id, "test_id");
1881 }
1882 _ => panic!("Expected Dio variant with id"),
1883 }
1884
1885 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
1887 EndpointConfigCli::Tap { name } => {
1888 assert_eq!(name, "tap0");
1889 }
1890 _ => panic!("Expected Tap variant"),
1891 }
1892
1893 assert!(EndpointConfigCli::from_str("invalid").is_err());
1895 }
1896
1897 #[test]
1898 fn test_nic_config_from_str() {
1899 use hvlite_defs::config::DeviceVtl;
1900
1901 let config = NicConfigCli::from_str("none").unwrap();
1903 assert_eq!(config.vtl, DeviceVtl::Vtl0);
1904 assert!(config.max_queues.is_none());
1905 assert!(!config.underhill);
1906 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1907
1908 let config = NicConfigCli::from_str("vtl2:none").unwrap();
1910 assert_eq!(config.vtl, DeviceVtl::Vtl2);
1911 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1912
1913 let config = NicConfigCli::from_str("queues=4:none").unwrap();
1915 assert_eq!(config.max_queues, Some(4));
1916 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1917
1918 let config = NicConfigCli::from_str("uh:none").unwrap();
1920 assert!(config.underhill);
1921 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1922
1923 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
1925 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); }
1927
1928 #[test]
1929 fn test_smt_config_from_str() {
1930 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
1931 assert_eq!(
1932 SmtConfigCli::from_str("force").unwrap(),
1933 SmtConfigCli::Force
1934 );
1935 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
1936
1937 assert!(SmtConfigCli::from_str("invalid").is_err());
1939 assert!(SmtConfigCli::from_str("").is_err());
1940 }
1941
1942 #[test]
1943 fn test_pcat_boot_order_from_str() {
1944 let order = PcatBootOrderCli::from_str("optical").unwrap();
1946 assert_eq!(order.0[0], PcatBootDevice::Optical);
1947
1948 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
1950 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
1951 assert_eq!(order.0[1], PcatBootDevice::Network);
1952
1953 assert!(PcatBootOrderCli::from_str("invalid").is_err());
1955 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
1957
1958 #[test]
1959 fn test_floppy_disk_from_str() {
1960 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
1962 assert!(!disk.read_only);
1963 match disk.kind {
1964 DiskCliKind::File {
1965 path,
1966 create_with_len,
1967 } => {
1968 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
1969 assert_eq!(create_with_len, None);
1970 }
1971 _ => panic!("Expected File variant"),
1972 }
1973
1974 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
1976 assert!(disk.read_only);
1977
1978 assert!(FloppyDiskCli::from_str("").is_err());
1980 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
1981 }
1982
1983 #[test]
1984 fn test_pcie_root_complex_from_str() {
1985 const ONE_MB: u64 = 1024 * 1024;
1986 const ONE_GB: u64 = 1024 * ONE_MB;
1987
1988 const DEFAULT_LOW_MMIO: u32 = (4 * ONE_MB) as u32;
1989 const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
1990
1991 assert_eq!(
1992 PcieRootComplexCli::from_str("rc0").unwrap(),
1993 PcieRootComplexCli {
1994 name: "rc0".to_string(),
1995 segment: 0,
1996 start_bus: 0,
1997 end_bus: 255,
1998 low_mmio: DEFAULT_LOW_MMIO,
1999 high_mmio: DEFAULT_HIGH_MMIO,
2000 }
2001 );
2002
2003 assert_eq!(
2004 PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
2005 PcieRootComplexCli {
2006 name: "rc1".to_string(),
2007 segment: 1,
2008 start_bus: 0,
2009 end_bus: 255,
2010 low_mmio: DEFAULT_LOW_MMIO,
2011 high_mmio: DEFAULT_HIGH_MMIO,
2012 }
2013 );
2014
2015 assert_eq!(
2016 PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
2017 PcieRootComplexCli {
2018 name: "rc2".to_string(),
2019 segment: 0,
2020 start_bus: 32,
2021 end_bus: 255,
2022 low_mmio: DEFAULT_LOW_MMIO,
2023 high_mmio: DEFAULT_HIGH_MMIO,
2024 }
2025 );
2026
2027 assert_eq!(
2028 PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
2029 PcieRootComplexCli {
2030 name: "rc3".to_string(),
2031 segment: 0,
2032 start_bus: 0,
2033 end_bus: 31,
2034 low_mmio: DEFAULT_LOW_MMIO,
2035 high_mmio: DEFAULT_HIGH_MMIO,
2036 }
2037 );
2038
2039 assert_eq!(
2040 PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
2041 PcieRootComplexCli {
2042 name: "rc4".to_string(),
2043 segment: 0,
2044 start_bus: 32,
2045 end_bus: 127,
2046 low_mmio: DEFAULT_LOW_MMIO,
2047 high_mmio: 2 * ONE_GB,
2048 }
2049 );
2050
2051 assert_eq!(
2052 PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
2053 PcieRootComplexCli {
2054 name: "rc5".to_string(),
2055 segment: 2,
2056 start_bus: 32,
2057 end_bus: 127,
2058 low_mmio: DEFAULT_LOW_MMIO,
2059 high_mmio: DEFAULT_HIGH_MMIO,
2060 }
2061 );
2062
2063 assert_eq!(
2064 PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
2065 PcieRootComplexCli {
2066 name: "rc6".to_string(),
2067 segment: 0,
2068 start_bus: 0,
2069 end_bus: 255,
2070 low_mmio: ONE_MB as u32,
2071 high_mmio: 64 * ONE_GB,
2072 }
2073 );
2074
2075 assert!(PcieRootComplexCli::from_str("").is_err());
2077 assert!(PcieRootComplexCli::from_str("poorly,").is_err());
2078 assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
2079 assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
2080 assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
2081 assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
2082 assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
2083 assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
2084 assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
2085 assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
2086 assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
2087 }
2088
2089 #[test]
2090 fn test_pcie_root_port_from_str() {
2091 assert_eq!(
2092 PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
2093 PcieRootPortCli {
2094 root_complex_name: "rc0".to_string(),
2095 name: "rc0rp0".to_string()
2096 }
2097 );
2098
2099 assert_eq!(
2100 PcieRootPortCli::from_str("my_rc:port2").unwrap(),
2101 PcieRootPortCli {
2102 root_complex_name: "my_rc".to_string(),
2103 name: "port2".to_string()
2104 }
2105 );
2106
2107 assert!(PcieRootPortCli::from_str("").is_err());
2109 assert!(PcieRootPortCli::from_str("rp0").is_err());
2110 assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
2111 assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
2112 assert!(PcieRootPortCli::from_str("rc0:rp0,rp3").is_err());
2113 }
2114}