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 Underhill
147"#)]
148 #[clap(long, value_name = "FILE")]
149 pub disk: Vec<DiskCli>,
150
151 #[clap(long_help = r#"
153e.g: --nvme memdiff:file:/path/to/disk.vhd
154
155syntax: \<path\> | kind:<arg>[,flag,opt=arg,...]
156
157valid disk kinds:
158 `mem:<len>` memory backed disk
159 <len>: length of ramdisk, e.g.: `1G`
160 `memdiff:<disk>` memory backed diff disk
161 <disk>: lower disk, e.g.: `file:base.img`
162 `file:\<path\>` file-backed disk
163 \<path\>: path to file
164
165flags:
166 `ro` open disk as read-only
167 `vtl2` assign this disk to VTL2
168"#)]
169 #[clap(long)]
170 pub nvme: Vec<DiskCli>,
171
172 #[clap(long, value_name = "COUNT", default_value = "0")]
174 pub scsi_sub_channels: u16,
175
176 #[clap(long)]
178 pub nic: bool,
179
180 #[clap(long)]
185 pub net: Vec<NicConfigCli>,
186
187 #[clap(long, value_name = "SWITCH_ID")]
191 pub kernel_vmnic: Vec<String>,
192
193 #[clap(long)]
195 pub gfx: bool,
196
197 #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
199 pub vtl2_gfx: bool,
200
201 #[clap(long)]
203 pub vnc: bool,
204
205 #[clap(long, value_name = "PORT", default_value = "5900")]
207 pub vnc_port: u16,
208
209 #[cfg(guest_arch = "x86_64")]
211 #[clap(long, default_value_t)]
212 pub apic_id_offset: u32,
213
214 #[clap(long)]
216 pub vps_per_socket: Option<u32>,
217
218 #[clap(long, default_value = "auto")]
220 pub smt: SmtConfigCli,
221
222 #[cfg(guest_arch = "x86_64")]
224 #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
225 pub x2apic: X2ApicConfig,
226
227 #[clap(long)]
229 pub virtio_console: bool,
230
231 #[clap(long, conflicts_with("virtio_console"))]
233 pub virtio_console_pci: bool,
234
235 #[clap(long, value_name = "SERIAL")]
237 pub com1: Option<SerialConfigCli>,
238
239 #[clap(long, value_name = "SERIAL")]
241 pub com2: Option<SerialConfigCli>,
242
243 #[clap(long, value_name = "SERIAL")]
245 pub com3: Option<SerialConfigCli>,
246
247 #[clap(long, value_name = "SERIAL")]
249 pub com4: Option<SerialConfigCli>,
250
251 #[clap(long, value_name = "SERIAL")]
253 pub virtio_serial: Option<SerialConfigCli>,
254
255 #[structopt(long, value_name = "SERIAL")]
257 pub vmbus_com1_serial: Option<SerialConfigCli>,
258
259 #[structopt(long, value_name = "SERIAL")]
261 pub vmbus_com2_serial: Option<SerialConfigCli>,
262
263 #[clap(long, value_name = "SERIAL")]
265 pub debugcon: Option<DebugconSerialConfigCli>,
266
267 #[clap(long, short = 'e')]
269 pub uefi: bool,
270
271 #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
273 pub uefi_firmware: OptionalPathBuf,
274
275 #[clap(long, requires("uefi"))]
277 pub uefi_debug: bool,
278
279 #[clap(long, requires("uefi"))]
281 pub uefi_enable_memory_protections: bool,
282
283 #[clap(long, requires("pcat"))]
294 pub pcat_boot_order: Option<PcatBootOrderCli>,
295
296 #[clap(long, conflicts_with("uefi"))]
298 pub pcat: bool,
299
300 #[clap(long, requires("pcat"), value_name = "FILE")]
302 pub pcat_firmware: Option<PathBuf>,
303
304 #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
306 pub igvm: Option<PathBuf>,
307
308 #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
311 pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
312
313 #[clap(long, value_name = "tag,root_path")]
315 pub virtio_9p: Vec<FsArgs>,
316
317 #[clap(long)]
319 pub virtio_9p_debug: bool,
320
321 #[clap(long, value_name = "tag,root_path,[options]")]
323 pub virtio_fs: Vec<FsArgsWithOptions>,
324
325 #[clap(long, value_name = "tag,root_path")]
327 pub virtio_fs_shmem: Vec<FsArgs>,
328
329 #[clap(long, value_name = "BUS", default_value = "auto")]
331 pub virtio_fs_bus: VirtioBusCli,
332
333 #[clap(long, value_name = "PATH")]
335 pub virtio_pmem: Option<String>,
336
337 #[clap(long)]
343 pub virtio_net: Vec<NicConfigCli>,
344
345 #[clap(long, value_name = "PATH")]
347 pub log_file: Option<PathBuf>,
348
349 #[clap(long, value_name = "SOCKETPATH")]
351 pub ttrpc: Option<PathBuf>,
352
353 #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
355 pub grpc: Option<PathBuf>,
356
357 #[clap(long)]
359 pub single_process: bool,
360
361 #[cfg(windows)]
363 #[clap(long, value_name = "PATH")]
364 pub device: Vec<String>,
365
366 #[clap(long, requires("uefi"))]
368 pub disable_frontpage: bool,
369
370 #[clap(long)]
372 pub tpm: bool,
373
374 #[clap(long, default_value = "control", hide(true))]
378 #[expect(clippy::option_option)]
379 pub internal_worker: Option<Option<String>>,
380
381 #[clap(long, requires("vtl2"))]
383 pub vmbus_redirect: bool,
384
385 #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
387 pub vmbus_max_version: Option<u32>,
388
389 #[clap(long_help = r#"
393e.g: --vmgs memdiff:file:/path/to/file.vmgs
394
395syntax: \<path\> | kind:<arg>[,flag]
396
397valid disk kinds:
398 `mem:<len>` memory backed disk
399 <len>: length of ramdisk, e.g.: `1G`
400 `memdiff:<disk>` memory backed diff disk
401 <disk>: lower disk, e.g.: `file:base.img`
402 `file:\<path\>` file-backed disk
403 \<path\>: path to file
404
405flags:
406 `fmt` reprovision the VMGS before boot
407 `fmt-on-fail` reprovision the VMGS before boot if it is corrupted
408"#)]
409 #[clap(long)]
410 pub vmgs: Option<VmgsCli>,
411
412 #[clap(long, requires("pcat"), value_name = "FILE")]
414 pub vga_firmware: Option<PathBuf>,
415
416 #[clap(long)]
418 pub secure_boot: bool,
419
420 #[clap(long)]
422 pub secure_boot_template: Option<SecureBootTemplateCli>,
423
424 #[clap(long, value_name = "PATH")]
426 pub custom_uefi_json: Option<PathBuf>,
427
428 #[clap(long, hide(true))]
433 pub relay_console_path: Option<PathBuf>,
434
435 #[clap(long, hide(true))]
439 pub relay_console_title: Option<String>,
440
441 #[clap(long, value_name = "PORT")]
443 pub gdb: Option<u16>,
444
445 #[clap(long)]
447 pub mana: Vec<NicConfigCli>,
448
449 #[clap(long, value_parser = parse_hypervisor)]
451 pub hypervisor: Option<Hypervisor>,
452
453 #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
461 pub custom_dsdt: Option<PathBuf>,
462
463 #[clap(long_help = r#"
473e.g: --ide memdiff:file:/path/to/disk.vhd
474
475syntax: \<path\> | kind:<arg>[,flag,opt=arg,...]
476
477valid disk kinds:
478 `mem:<len>` memory backed disk
479 <len>: length of ramdisk, e.g.: `1G`
480 `memdiff:<disk>` memory backed diff disk
481 <disk>: lower disk, e.g.: `file:base.img`
482 `file:\<path\>` file-backed disk
483 \<path\>: path to file
484
485flags:
486 `ro` open disk as read-only
487 `s` attach drive to secondary ide channel
488 `dvd` specifies that device is cd/dvd and it is read_only
489"#)]
490 #[clap(long, value_name = "FILE")]
491 pub ide: Vec<IdeDiskCli>,
492
493 #[clap(long_help = r#"
496e.g: --floppy memdiff:/path/to/disk.vfd,ro
497
498syntax: \<path\> | kind:<arg>[,flag,opt=arg,...]
499
500valid disk kinds:
501 `mem:<len>` memory backed disk
502 <len>: length of ramdisk, e.g.: `1G`
503 `memdiff:<disk>` memory backed diff disk
504 <disk>: lower disk, e.g.: `file:base.img`
505 `file:\<path\>` file-backed disk
506 \<path\>: path to file
507
508flags:
509 `ro` open disk as read-only
510"#)]
511 #[clap(long, value_name = "FILE", requires("pcat"), conflicts_with("uefi"))]
512 pub floppy: Vec<FloppyDiskCli>,
513
514 #[clap(long)]
516 pub guest_watchdog: bool,
517
518 #[clap(long)]
520 pub openhcl_dump_path: Option<PathBuf>,
521
522 #[clap(long)]
524 pub halt_on_reset: bool,
525
526 #[clap(long)]
528 pub write_saved_state_proto: Option<PathBuf>,
529
530 #[clap(long)]
532 pub imc: Option<PathBuf>,
533
534 #[clap(long)]
536 pub mcr: bool, #[clap(long)]
540 pub battery: bool,
541
542 #[clap(long)]
544 pub uefi_console_mode: Option<UefiConsoleModeCli>,
545
546 #[clap(long)]
548 pub default_boot_always_attempt: bool,
549}
550
551#[derive(Clone)]
552pub struct FsArgs {
553 pub tag: String,
554 pub path: String,
555}
556
557impl FromStr for FsArgs {
558 type Err = anyhow::Error;
559
560 fn from_str(s: &str) -> Result<Self, Self::Err> {
561 let mut s = s.split(',');
562 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
563 anyhow::bail!("expected <tag>,<path>");
564 };
565 Ok(Self {
566 tag: tag.to_owned(),
567 path: path.to_owned(),
568 })
569 }
570}
571
572#[derive(Clone)]
573pub struct FsArgsWithOptions {
574 pub tag: String,
576 pub path: String,
578 pub options: String,
580}
581
582impl FromStr for FsArgsWithOptions {
583 type Err = anyhow::Error;
584
585 fn from_str(s: &str) -> Result<Self, Self::Err> {
586 let mut s = s.split(',');
587 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
588 anyhow::bail!("expected <tag>,<path>[,<options>]");
589 };
590 let options = s.collect::<Vec<_>>().join(";");
591 Ok(Self {
592 tag: tag.to_owned(),
593 path: path.to_owned(),
594 options,
595 })
596 }
597}
598
599#[derive(Copy, Clone, clap::ValueEnum)]
600pub enum VirtioBusCli {
601 Auto,
602 Mmio,
603 Pci,
604 Vpci,
605}
606
607#[derive(clap::ValueEnum, Clone, Copy)]
608pub enum SecureBootTemplateCli {
609 Windows,
610 UefiCa,
611}
612
613fn parse_memory(s: &str) -> anyhow::Result<u64> {
614 || -> Option<u64> {
615 let mut b = s.as_bytes();
616 if s.ends_with('B') {
617 b = &b[..b.len() - 1]
618 }
619 if b.is_empty() {
620 return None;
621 }
622 let multi = match b[b.len() - 1] as char {
623 'T' => Some(1024 * 1024 * 1024 * 1024),
624 'G' => Some(1024 * 1024 * 1024),
625 'M' => Some(1024 * 1024),
626 'K' => Some(1024),
627 _ => None,
628 };
629 if multi.is_some() {
630 b = &b[..b.len() - 1]
631 }
632 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
633 Some(n * multi.unwrap_or(1))
634 }()
635 .with_context(|| format!("invalid memory size '{0}'", s))
636}
637
638fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
640 match s.strip_prefix("0x") {
641 Some(rest) => u64::from_str_radix(rest, 16),
642 None => s.parse::<u64>(),
643 }
644}
645
646#[derive(Clone)]
647pub enum DiskCliKind {
648 Memory(u64),
650 MemoryDiff(Box<DiskCliKind>),
652 Sqlite {
654 path: PathBuf,
655 create_with_len: Option<u64>,
656 },
657 SqliteDiff {
659 path: PathBuf,
660 create: bool,
661 disk: Box<DiskCliKind>,
662 },
663 AutoCacheSqlite {
665 cache_path: String,
666 key: Option<String>,
667 disk: Box<DiskCliKind>,
668 },
669 PersistentReservationsWrapper(Box<DiskCliKind>),
671 File {
673 path: PathBuf,
674 create_with_len: Option<u64>,
675 },
676 Blob {
678 kind: BlobKind,
679 url: String,
680 },
681 Crypt {
683 cipher: DiskCipher,
684 key_file: PathBuf,
685 disk: Box<DiskCliKind>,
686 },
687}
688
689#[derive(ValueEnum, Clone, Copy)]
690pub enum DiskCipher {
691 #[clap(name = "xts-aes-256")]
692 XtsAes256,
693}
694
695#[derive(Copy, Clone)]
696pub enum BlobKind {
697 Flat,
698 Vhd1,
699}
700
701fn parse_path_and_len(arg: &str) -> anyhow::Result<(PathBuf, Option<u64>)> {
702 Ok(match arg.split_once(';') {
703 Some((path, len)) => {
704 let Some(len) = len.strip_prefix("create=") else {
705 anyhow::bail!("invalid syntax after ';', expected 'create=<len>'")
706 };
707
708 let len: u64 = if len == "VMGS_DEFAULT" {
709 vmgs_format::VMGS_DEFAULT_CAPACITY
710 } else {
711 parse_memory(len)?
712 };
713
714 (path.into(), Some(len))
715 }
716 None => (arg.into(), None),
717 })
718}
719
720impl FromStr for DiskCliKind {
721 type Err = anyhow::Error;
722
723 fn from_str(s: &str) -> anyhow::Result<Self> {
724 let disk = match s.split_once(':') {
725 None => {
727 let (path, create_with_len) = parse_path_and_len(s)?;
728 DiskCliKind::File {
729 path,
730 create_with_len,
731 }
732 }
733 Some((kind, arg)) => match kind {
734 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
735 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
736 "sql" => {
737 let (path, create_with_len) = parse_path_and_len(arg)?;
738 DiskCliKind::Sqlite {
739 path,
740 create_with_len,
741 }
742 }
743 "sqldiff" => {
744 let (path_and_opts, kind) =
745 arg.split_once(':').context("expected path[;opts]:kind")?;
746 let disk = Box::new(kind.parse()?);
747 match path_and_opts.split_once(';') {
748 Some((path, create)) => {
749 if create != "create" {
750 anyhow::bail!("invalid syntax after ';', expected 'create'")
751 }
752 DiskCliKind::SqliteDiff {
753 path: path.into(),
754 create: true,
755 disk,
756 }
757 }
758 None => DiskCliKind::SqliteDiff {
759 path: path_and_opts.into(),
760 create: false,
761 disk,
762 },
763 }
764 }
765 "autocache" => {
766 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
767 let cache_path = std::env::var("OPENVMM_AUTO_CACHE_PATH")
768 .context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
769 DiskCliKind::AutoCacheSqlite {
770 cache_path,
771 key: (!key.is_empty()).then(|| key.to_string()),
772 disk: Box::new(kind.parse()?),
773 }
774 }
775 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
776 "file" => {
777 let (path, create_with_len) = parse_path_and_len(s)?;
778 DiskCliKind::File {
779 path,
780 create_with_len,
781 }
782 }
783 "blob" => {
784 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
785 let blob_kind = match blob_kind {
786 "flat" => BlobKind::Flat,
787 "vhd1" => BlobKind::Vhd1,
788 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
789 };
790 DiskCliKind::Blob {
791 kind: blob_kind,
792 url: url.to_string(),
793 }
794 }
795 "crypt" => {
796 let (cipher, (key, kind)) = arg
797 .split_once(':')
798 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
799 .context("expected cipher:key_file:kind")?;
800 DiskCliKind::Crypt {
801 cipher: ValueEnum::from_str(cipher, false)
802 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
803 key_file: PathBuf::from(key),
804 disk: Box::new(kind.parse()?),
805 }
806 }
807 kind => {
808 let (path, create_with_len) = parse_path_and_len(s)?;
813 if path.has_root() {
814 DiskCliKind::File {
815 path,
816 create_with_len,
817 }
818 } else {
819 anyhow::bail!("invalid disk kind {kind}");
820 }
821 }
822 },
823 };
824 Ok(disk)
825 }
826}
827
828#[derive(Clone)]
829pub struct VmgsCli {
830 pub kind: DiskCliKind,
831 pub provision: ProvisionVmgs,
832}
833
834#[derive(Copy, Clone)]
835pub enum ProvisionVmgs {
836 OnEmpty,
837 OnFailure,
838 True,
839}
840
841impl FromStr for VmgsCli {
842 type Err = anyhow::Error;
843
844 fn from_str(s: &str) -> anyhow::Result<Self> {
845 let (kind, opt) = s
846 .split_once(',')
847 .map(|(k, o)| (k, Some(o)))
848 .unwrap_or((s, None));
849 let kind = kind.parse()?;
850
851 let provision = match opt {
852 None => ProvisionVmgs::OnEmpty,
853 Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
854 Some("fmt") => ProvisionVmgs::True,
855 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
856 };
857
858 Ok(VmgsCli { kind, provision })
859 }
860}
861
862#[derive(Clone)]
864pub struct DiskCli {
865 pub vtl: DeviceVtl,
866 pub kind: DiskCliKind,
867 pub read_only: bool,
868 pub is_dvd: bool,
869 pub underhill: Option<UnderhillDiskSource>,
870}
871
872#[derive(Copy, Clone)]
873pub enum UnderhillDiskSource {
874 Scsi,
875 Nvme,
876}
877
878impl FromStr for DiskCli {
879 type Err = anyhow::Error;
880
881 fn from_str(s: &str) -> anyhow::Result<Self> {
882 let mut opts = s.split(',');
883 let kind = opts.next().unwrap().parse()?;
884
885 let mut read_only = false;
886 let mut is_dvd = false;
887 let mut underhill = None;
888 let mut vtl = DeviceVtl::Vtl0;
889 for opt in opts {
890 let mut s = opt.split('=');
891 let opt = s.next().unwrap();
892 match opt {
893 "ro" => read_only = true,
894 "dvd" => {
895 is_dvd = true;
896 read_only = true;
897 }
898 "vtl2" => {
899 vtl = DeviceVtl::Vtl2;
900 }
901 "uh" => underhill = Some(UnderhillDiskSource::Scsi),
902 "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
903 opt => anyhow::bail!("unknown option: '{opt}'"),
904 }
905 }
906
907 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
908 anyhow::bail!("`uh` is incompatible with `vtl2`");
909 }
910
911 Ok(DiskCli {
912 vtl,
913 kind,
914 read_only,
915 is_dvd,
916 underhill,
917 })
918 }
919}
920
921#[derive(Clone)]
923pub struct IdeDiskCli {
924 pub kind: DiskCliKind,
925 pub read_only: bool,
926 pub channel: Option<u8>,
927 pub device: Option<u8>,
928 pub is_dvd: bool,
929}
930
931impl FromStr for IdeDiskCli {
932 type Err = anyhow::Error;
933
934 fn from_str(s: &str) -> anyhow::Result<Self> {
935 let mut opts = s.split(',');
936 let kind = opts.next().unwrap().parse()?;
937
938 let mut read_only = false;
939 let mut channel = None;
940 let mut device = None;
941 let mut is_dvd = false;
942 for opt in opts {
943 let mut s = opt.split('=');
944 let opt = s.next().unwrap();
945 match opt {
946 "ro" => read_only = true,
947 "p" => channel = Some(0),
948 "s" => channel = Some(1),
949 "0" => device = Some(0),
950 "1" => device = Some(1),
951 "dvd" => {
952 is_dvd = true;
953 read_only = true;
954 }
955 _ => anyhow::bail!("unknown option: '{opt}'"),
956 }
957 }
958
959 Ok(IdeDiskCli {
960 kind,
961 read_only,
962 channel,
963 device,
964 is_dvd,
965 })
966 }
967}
968
969#[derive(Clone)]
971pub struct FloppyDiskCli {
972 pub kind: DiskCliKind,
973 pub read_only: bool,
974}
975
976impl FromStr for FloppyDiskCli {
977 type Err = anyhow::Error;
978
979 fn from_str(s: &str) -> anyhow::Result<Self> {
980 let mut opts = s.split(',');
981 let kind = opts.next().unwrap().parse()?;
982
983 let mut read_only = false;
984 for opt in opts {
985 let mut s = opt.split('=');
986 let opt = s.next().unwrap();
987 match opt {
988 "ro" => read_only = true,
989 _ => anyhow::bail!("unknown option: '{opt}'"),
990 }
991 }
992
993 Ok(FloppyDiskCli { kind, read_only })
994 }
995}
996
997#[derive(Clone)]
998pub struct DebugconSerialConfigCli {
999 pub port: u16,
1000 pub serial: SerialConfigCli,
1001}
1002
1003impl FromStr for DebugconSerialConfigCli {
1004 type Err = String;
1005
1006 fn from_str(s: &str) -> Result<Self, Self::Err> {
1007 let Some((port, serial)) = s.split_once(',') else {
1008 return Err("invalid format (missing comma between port and serial)".into());
1009 };
1010
1011 let port: u16 = parse_number(port)
1012 .map_err(|_| "could not parse port".to_owned())?
1013 .try_into()
1014 .map_err(|_| "port must be 16-bit")?;
1015 let serial: SerialConfigCli = serial.parse()?;
1016
1017 Ok(Self { port, serial })
1018 }
1019}
1020
1021#[derive(Clone)]
1023pub enum SerialConfigCli {
1024 None,
1025 Console,
1026 NewConsole(Option<PathBuf>, Option<String>),
1027 Stderr,
1028 Pipe(PathBuf),
1029 Tcp(SocketAddr),
1030}
1031
1032impl FromStr for SerialConfigCli {
1033 type Err = String;
1034
1035 fn from_str(s: &str) -> Result<Self, Self::Err> {
1036 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1037
1038 let first_key = match keyvalues.first() {
1039 Some(first_pair) => first_pair.0.as_str(),
1040 None => Err("invalid serial configuration: no values supplied")?,
1041 };
1042 let first_value = keyvalues.first().unwrap().1.as_ref();
1043
1044 let ret = match first_key {
1045 "none" => SerialConfigCli::None,
1046 "console" => SerialConfigCli::Console,
1047 "stderr" => SerialConfigCli::Stderr,
1048 "term" => match first_value {
1049 Some(path) => {
1050 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1052 let window_name = match window_name {
1053 Some((_, Some(name))) => Some(name.clone()),
1054 _ => None,
1055 };
1056
1057 SerialConfigCli::NewConsole(Some(path.into()), window_name)
1058 }
1059 None => SerialConfigCli::NewConsole(None, None),
1060 },
1061 "listen" => match first_value {
1062 Some(path) => {
1063 if let Some(tcp) = path.strip_prefix("tcp:") {
1064 let addr = tcp
1065 .parse()
1066 .map_err(|err| format!("invalid tcp address: {err}"))?;
1067 SerialConfigCli::Tcp(addr)
1068 } else {
1069 SerialConfigCli::Pipe(s.into())
1070 }
1071 }
1072 None => Err(
1073 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1074 )?,
1075 },
1076 _ => {
1077 return Err(format!(
1078 "invalid serial configuration: '{}' is not a known option",
1079 first_key
1080 ));
1081 }
1082 };
1083
1084 Ok(ret)
1085 }
1086}
1087
1088impl SerialConfigCli {
1089 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1092 let mut ret = Vec::new();
1093
1094 for item in s.split(',') {
1096 let mut eqsplit = item.split('=');
1099 let key = eqsplit.next();
1100 let value = eqsplit.next();
1101
1102 if let Some(key) = key {
1103 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1104 } else {
1105 return Err("invalid key=value pair in serial config".into());
1107 }
1108 }
1109 Ok(ret)
1110 }
1111}
1112
1113#[derive(Clone)]
1114pub enum EndpointConfigCli {
1115 None,
1116 Consomme { cidr: Option<String> },
1117 Dio { id: Option<String> },
1118 Tap { name: String },
1119}
1120
1121impl FromStr for EndpointConfigCli {
1122 type Err = String;
1123
1124 fn from_str(s: &str) -> Result<Self, Self::Err> {
1125 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1126 ["none"] => EndpointConfigCli::None,
1127 ["consomme", s @ ..] => EndpointConfigCli::Consomme {
1128 cidr: s.first().map(|&s| s.to_owned()),
1129 },
1130 ["dio", s @ ..] => EndpointConfigCli::Dio {
1131 id: s.first().map(|s| (*s).to_owned()),
1132 },
1133 ["tap", name] => EndpointConfigCli::Tap {
1134 name: (*name).to_owned(),
1135 },
1136 _ => return Err("invalid network backend".into()),
1137 };
1138
1139 Ok(ret)
1140 }
1141}
1142
1143#[derive(Clone)]
1144pub struct NicConfigCli {
1145 pub vtl: DeviceVtl,
1146 pub endpoint: EndpointConfigCli,
1147 pub max_queues: Option<u16>,
1148 pub underhill: bool,
1149}
1150
1151impl FromStr for NicConfigCli {
1152 type Err = String;
1153
1154 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1155 let mut vtl = DeviceVtl::Vtl0;
1156 let mut max_queues = None;
1157 let mut underhill = false;
1158 while let Some((opt, rest)) = s.split_once(':') {
1159 if let Some((opt, val)) = opt.split_once('=') {
1160 match opt {
1161 "queues" => {
1162 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1163 }
1164 _ => break,
1165 }
1166 } else {
1167 match opt {
1168 "vtl2" => {
1169 vtl = DeviceVtl::Vtl2;
1170 }
1171 "uh" => underhill = true,
1172 _ => break,
1173 }
1174 }
1175 s = rest;
1176 }
1177
1178 if underhill && vtl != DeviceVtl::Vtl0 {
1179 return Err("`uh` is incompatible with `vtl2`".into());
1180 }
1181
1182 let endpoint = s.parse()?;
1183 Ok(NicConfigCli {
1184 vtl,
1185 endpoint,
1186 max_queues,
1187 underhill,
1188 })
1189 }
1190}
1191
1192#[derive(Debug, Error)]
1193#[error("unknown hypervisor: {0}")]
1194pub struct UnknownHypervisor(String);
1195
1196fn parse_hypervisor(s: &str) -> Result<Hypervisor, UnknownHypervisor> {
1197 match s {
1198 "kvm" => Ok(Hypervisor::Kvm),
1199 "mshv" => Ok(Hypervisor::MsHv),
1200 "whp" => Ok(Hypervisor::Whp),
1201 _ => Err(UnknownHypervisor(s.to_owned())),
1202 }
1203}
1204
1205#[derive(Debug, Error)]
1206#[error("unknown VTL2 relocation type: {0}")]
1207pub struct UnknownVtl2RelocationType(String);
1208
1209fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1210 match s {
1211 "disable" => Ok(Vtl2BaseAddressType::File),
1212 s if s.starts_with("auto=") => {
1213 let s = s.strip_prefix("auto=").unwrap_or_default();
1214 let size = if s == "filesize" {
1215 None
1216 } else {
1217 let size = parse_memory(s).map_err(|e| {
1218 UnknownVtl2RelocationType(format!(
1219 "unable to parse memory size from {} for 'auto=' type, {e}",
1220 e
1221 ))
1222 })?;
1223 Some(size)
1224 };
1225 Ok(Vtl2BaseAddressType::MemoryLayout { size })
1226 }
1227 s if s.starts_with("absolute=") => {
1228 let s = s.strip_prefix("absolute=");
1229 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
1230 UnknownVtl2RelocationType(format!(
1231 "unable to parse number from {} for 'absolute=' type",
1232 e
1233 ))
1234 })?;
1235 Ok(Vtl2BaseAddressType::Absolute(addr))
1236 }
1237 s if s.starts_with("vtl2=") => {
1238 let s = s.strip_prefix("vtl2=").unwrap_or_default();
1239 let size = if s == "filesize" {
1240 None
1241 } else {
1242 let size = parse_memory(s).map_err(|e| {
1243 UnknownVtl2RelocationType(format!(
1244 "unable to parse memory size from {} for 'vtl2=' type, {e}",
1245 e
1246 ))
1247 })?;
1248 Some(size)
1249 };
1250 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
1251 }
1252 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
1253 }
1254}
1255
1256#[derive(Debug, Copy, Clone)]
1257pub enum SmtConfigCli {
1258 Auto,
1259 Force,
1260 Off,
1261}
1262
1263#[derive(Debug, Error)]
1264#[error("expected auto, force, or off")]
1265pub struct BadSmtConfig;
1266
1267impl FromStr for SmtConfigCli {
1268 type Err = BadSmtConfig;
1269
1270 fn from_str(s: &str) -> Result<Self, Self::Err> {
1271 let r = match s {
1272 "auto" => Self::Auto,
1273 "force" => Self::Force,
1274 "off" => Self::Off,
1275 _ => return Err(BadSmtConfig),
1276 };
1277 Ok(r)
1278 }
1279}
1280
1281#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
1282fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
1283 let r = match s {
1284 "auto" => X2ApicConfig::Auto,
1285 "supported" => X2ApicConfig::Supported,
1286 "off" => X2ApicConfig::Unsupported,
1287 "on" => X2ApicConfig::Enabled,
1288 _ => return Err("expected auto, supported, off, or on"),
1289 };
1290 Ok(r)
1291}
1292
1293#[derive(Debug, Copy, Clone, ValueEnum)]
1294pub enum Vtl0LateMapPolicyCli {
1295 Off,
1296 Log,
1297 Halt,
1298 Exception,
1299}
1300
1301#[derive(Debug, Copy, Clone, ValueEnum)]
1302pub enum IsolationCli {
1303 Vbs,
1304}
1305
1306#[derive(Debug, Copy, Clone)]
1307pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
1308
1309impl FromStr for PcatBootOrderCli {
1310 type Err = &'static str;
1311
1312 fn from_str(s: &str) -> Result<Self, Self::Err> {
1313 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
1314 let mut order = Vec::new();
1315
1316 for item in s.split(',') {
1317 let device = match item {
1318 "optical" => PcatBootDevice::Optical,
1319 "hdd" => PcatBootDevice::HardDrive,
1320 "net" => PcatBootDevice::Network,
1321 "floppy" => PcatBootDevice::Floppy,
1322 _ => return Err("unknown boot device type"),
1323 };
1324
1325 let default_pos = default_order
1326 .iter()
1327 .position(|x| x == &Some(device))
1328 .ok_or("cannot pass duplicate boot devices")?;
1329
1330 order.push(default_order[default_pos].take().unwrap());
1331 }
1332
1333 order.extend(default_order.into_iter().flatten());
1334 assert_eq!(order.len(), 4);
1335
1336 Ok(Self(order.try_into().unwrap()))
1337 }
1338}
1339
1340#[derive(Copy, Clone, Debug, ValueEnum)]
1341pub enum UefiConsoleModeCli {
1342 Default,
1343 Com1,
1344 Com2,
1345 None,
1346}
1347
1348fn default_value_from_arch_env(name: &str) -> OsString {
1356 let prefix = if cfg!(guest_arch = "x86_64") {
1357 "X86_64"
1358 } else if cfg!(guest_arch = "aarch64") {
1359 "AARCH64"
1360 } else {
1361 return Default::default();
1362 };
1363 let prefixed = format!("{}_{}", prefix, name);
1364 std::env::var_os(name)
1365 .or_else(|| std::env::var_os(prefixed))
1366 .unwrap_or_default()
1367}
1368
1369#[derive(Clone)]
1371pub struct OptionalPathBuf(pub Option<PathBuf>);
1372
1373impl From<&std::ffi::OsStr> for OptionalPathBuf {
1374 fn from(s: &std::ffi::OsStr) -> Self {
1375 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
1376 }
1377}