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)]
104 pub get_vmgs: Option<DiskCliKind>,
105
106 #[clap(long, requires("vtl2"))]
108 pub no_alias_map: bool,
109
110 #[clap(long, requires("vtl2"))]
112 pub isolation: Option<IsolationCli>,
113
114 #[clap(long, value_name = "PATH")]
116 pub vsock_path: Option<String>,
117
118 #[clap(long, value_name = "PATH", requires("vtl2"))]
120 pub vtl2_vsock_path: Option<String>,
121
122 #[clap(long, requires("vtl2"), default_value = "halt")]
124 pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
125
126 #[clap(long)]
128 pub no_enlightenments: bool,
129
130 #[clap(long)]
132 pub user_mode_apic: bool,
133
134 #[clap(long_help = r#"
136e.g: --disk memdiff:file:/path/to/disk.vhd
137
138syntax: \<path\> | kind:<arg>[,flag,opt=arg,...]
139
140valid disk kinds:
141 `mem:<len>` memory backed disk
142 <len>: length of ramdisk, e.g.: `1G`
143 `memdiff:<disk>` memory backed diff disk
144 <disk>: lower disk, e.g.: `file:base.img`
145 `file:\<path\>` file-backed disk
146 \<path\>: path to file
147
148flags:
149 `ro` open disk as read-only
150 `dvd` specifies that device is cd/dvd and it is read_only
151 `vtl2` assign this disk to VTL2
152 `uh` relay this disk to VTL0 through Underhill
153"#)]
154 #[clap(long, value_name = "FILE")]
155 pub disk: Vec<DiskCli>,
156
157 #[clap(long_help = r#"
159e.g: --nvme memdiff:file:/path/to/disk.vhd
160
161syntax: \<path\> | kind:<arg>[,flag,opt=arg,...]
162
163valid disk kinds:
164 `mem:<len>` memory backed disk
165 <len>: length of ramdisk, e.g.: `1G`
166 `memdiff:<disk>` memory backed diff disk
167 <disk>: lower disk, e.g.: `file:base.img`
168 `file:\<path\>` file-backed disk
169 \<path\>: path to file
170
171flags:
172 `ro` open disk as read-only
173 `vtl2` assign this disk to VTL2
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, value_name = "PATH")]
397 pub vmgs_file: Option<PathBuf>,
398
399 #[clap(long, requires("pcat"), value_name = "FILE")]
401 pub vga_firmware: Option<PathBuf>,
402
403 #[clap(long)]
405 pub secure_boot: bool,
406
407 #[clap(long)]
409 pub secure_boot_template: Option<SecureBootTemplateCli>,
410
411 #[clap(long, value_name = "PATH")]
413 pub custom_uefi_json: Option<PathBuf>,
414
415 #[clap(long, hide(true))]
420 pub relay_console_path: Option<PathBuf>,
421
422 #[clap(long, hide(true))]
426 pub relay_console_title: Option<String>,
427
428 #[clap(long, value_name = "PORT")]
430 pub gdb: Option<u16>,
431
432 #[clap(long)]
434 pub mana: Vec<NicConfigCli>,
435
436 #[clap(long, value_parser = parse_hypervisor)]
438 pub hypervisor: Option<Hypervisor>,
439
440 #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
448 pub custom_dsdt: Option<PathBuf>,
449
450 #[clap(long_help = r#"
460e.g: --ide memdiff:file:/path/to/disk.vhd
461
462syntax: \<path\> | kind:<arg>[,flag,opt=arg,...]
463
464valid disk kinds:
465 `mem:<len>` memory backed disk
466 <len>: length of ramdisk, e.g.: `1G`
467 `memdiff:<disk>` memory backed diff disk
468 <disk>: lower disk, e.g.: `file:base.img`
469 `file:\<path\>` file-backed disk
470 \<path\>: path to file
471
472flags:
473 `ro` open disk as read-only
474 `s` attach drive to secondary ide channel
475 `dvd` specifies that device is cd/dvd and it is read_only
476"#)]
477 #[clap(long, value_name = "FILE")]
478 pub ide: Vec<IdeDiskCli>,
479
480 #[clap(long_help = r#"
483e.g: --floppy memdiff:/path/to/disk.vfd,ro
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"#)]
498 #[clap(long, value_name = "FILE", requires("pcat"), conflicts_with("uefi"))]
499 pub floppy: Vec<FloppyDiskCli>,
500
501 #[clap(long)]
503 pub guest_watchdog: bool,
504
505 #[clap(long)]
507 pub openhcl_dump_path: Option<PathBuf>,
508
509 #[clap(long)]
511 pub halt_on_reset: bool,
512
513 #[clap(long)]
515 pub write_saved_state_proto: Option<PathBuf>,
516
517 #[clap(long)]
519 pub imc: Option<PathBuf>,
520
521 #[clap(long)]
523 pub mcr: bool, #[clap(long)]
527 pub battery: bool,
528
529 #[clap(long)]
531 pub uefi_console_mode: Option<UefiConsoleModeCli>,
532
533 #[clap(long)]
535 pub default_boot_always_attempt: bool,
536}
537
538#[derive(Clone)]
539pub struct FsArgs {
540 pub tag: String,
541 pub path: String,
542}
543
544impl FromStr for FsArgs {
545 type Err = anyhow::Error;
546
547 fn from_str(s: &str) -> Result<Self, Self::Err> {
548 let mut s = s.split(',');
549 let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
550 anyhow::bail!("expected <tag>,<path>");
551 };
552 Ok(Self {
553 tag: tag.to_owned(),
554 path: path.to_owned(),
555 })
556 }
557}
558
559#[derive(Clone)]
560pub struct FsArgsWithOptions {
561 pub tag: String,
563 pub path: String,
565 pub options: String,
567}
568
569impl FromStr for FsArgsWithOptions {
570 type Err = anyhow::Error;
571
572 fn from_str(s: &str) -> Result<Self, Self::Err> {
573 let mut s = s.split(',');
574 let (Some(tag), Some(path)) = (s.next(), s.next()) else {
575 anyhow::bail!("expected <tag>,<path>[,<options>]");
576 };
577 let options = s.collect::<Vec<_>>().join(";");
578 Ok(Self {
579 tag: tag.to_owned(),
580 path: path.to_owned(),
581 options,
582 })
583 }
584}
585
586#[derive(Copy, Clone, clap::ValueEnum)]
587pub enum VirtioBusCli {
588 Auto,
589 Mmio,
590 Pci,
591 Vpci,
592}
593
594#[derive(clap::ValueEnum, Clone, Copy)]
595pub enum SecureBootTemplateCli {
596 Windows,
597 UefiCa,
598}
599
600fn parse_memory(s: &str) -> anyhow::Result<u64> {
601 || -> Option<u64> {
602 let mut b = s.as_bytes();
603 if s.ends_with('B') {
604 b = &b[..b.len() - 1]
605 }
606 if b.is_empty() {
607 return None;
608 }
609 let multi = match b[b.len() - 1] as char {
610 'T' => Some(1024 * 1024 * 1024 * 1024),
611 'G' => Some(1024 * 1024 * 1024),
612 'M' => Some(1024 * 1024),
613 'K' => Some(1024),
614 _ => None,
615 };
616 if multi.is_some() {
617 b = &b[..b.len() - 1]
618 }
619 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
620 Some(n * multi.unwrap_or(1))
621 }()
622 .with_context(|| format!("invalid memory size '{0}'", s))
623}
624
625fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
627 match s.strip_prefix("0x") {
628 Some(rest) => u64::from_str_radix(rest, 16),
629 None => s.parse::<u64>(),
630 }
631}
632
633#[derive(Clone)]
634pub enum DiskCliKind {
635 Memory(u64),
637 MemoryDiff(Box<DiskCliKind>),
639 Sqlite {
641 path: PathBuf,
642 create_with_len: Option<u64>,
643 },
644 SqliteDiff {
646 path: PathBuf,
647 create: bool,
648 disk: Box<DiskCliKind>,
649 },
650 AutoCacheSqlite {
652 cache_path: String,
653 key: Option<String>,
654 disk: Box<DiskCliKind>,
655 },
656 PersistentReservationsWrapper(Box<DiskCliKind>),
658 File(PathBuf),
660 Blob {
662 kind: BlobKind,
663 url: String,
664 },
665 Crypt {
667 cipher: DiskCipher,
668 key_file: PathBuf,
669 disk: Box<DiskCliKind>,
670 },
671}
672
673#[derive(ValueEnum, Clone, Copy)]
674pub enum DiskCipher {
675 #[clap(name = "xts-aes-256")]
676 XtsAes256,
677}
678
679#[derive(Copy, Clone)]
680pub enum BlobKind {
681 Flat,
682 Vhd1,
683}
684
685impl FromStr for DiskCliKind {
686 type Err = anyhow::Error;
687
688 fn from_str(s: &str) -> anyhow::Result<Self> {
689 let disk = match s.split_once(':') {
690 None => DiskCliKind::File(PathBuf::from(s)),
692 Some((kind, arg)) => match kind {
693 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
694 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
695 "sql" => match arg.split_once(';') {
696 Some((path, len)) => {
697 let Some(len) = len.strip_prefix("create=") else {
698 anyhow::bail!("invalid syntax after ';', expected 'create=<len>'")
699 };
700
701 DiskCliKind::Sqlite {
702 path: path.into(),
703 create_with_len: Some(parse_memory(len)?),
704 }
705 }
706 None => DiskCliKind::Sqlite {
707 path: arg.into(),
708 create_with_len: None,
709 },
710 },
711 "sqldiff" => {
712 let (path_and_opts, kind) =
713 arg.split_once(':').context("expected path[;opts]:kind")?;
714 let disk = Box::new(kind.parse()?);
715
716 match path_and_opts.split_once(';') {
717 Some((path, create)) => {
718 if create != "create" {
719 anyhow::bail!("invalid syntax after ';', expected 'create'")
720 }
721 DiskCliKind::SqliteDiff {
722 path: path.into(),
723 create: true,
724 disk,
725 }
726 }
727 None => DiskCliKind::SqliteDiff {
728 path: path_and_opts.into(),
729 create: false,
730 disk,
731 },
732 }
733 }
734 "autocache" => {
735 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
736 let cache_path = std::env::var("OPENVMM_AUTO_CACHE_PATH")
737 .context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
738 DiskCliKind::AutoCacheSqlite {
739 cache_path,
740 key: (!key.is_empty()).then(|| key.to_string()),
741 disk: Box::new(kind.parse()?),
742 }
743 }
744 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
745 "file" => DiskCliKind::File(PathBuf::from(arg)),
746 "blob" => {
747 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
748 let blob_kind = match blob_kind {
749 "flat" => BlobKind::Flat,
750 "vhd1" => BlobKind::Vhd1,
751 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
752 };
753 DiskCliKind::Blob {
754 kind: blob_kind,
755 url: url.to_string(),
756 }
757 }
758 "crypt" => {
759 let (cipher, (key, kind)) = arg
760 .split_once(':')
761 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
762 .context("expected cipher:key_file:kind")?;
763 DiskCliKind::Crypt {
764 cipher: ValueEnum::from_str(cipher, false)
765 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
766 key_file: PathBuf::from(key),
767 disk: Box::new(kind.parse()?),
768 }
769 }
770 kind => {
771 let path_buf = PathBuf::from(s);
776 if path_buf.has_root() {
777 DiskCliKind::File(path_buf)
778 } else {
779 anyhow::bail!("invalid disk kind {kind}");
780 }
781 }
782 },
783 };
784 Ok(disk)
785 }
786}
787
788#[derive(Clone)]
790pub struct DiskCli {
791 pub vtl: DeviceVtl,
792 pub kind: DiskCliKind,
793 pub read_only: bool,
794 pub is_dvd: bool,
795 pub underhill: Option<UnderhillDiskSource>,
796}
797
798#[derive(Copy, Clone)]
799pub enum UnderhillDiskSource {
800 Scsi,
801 Nvme,
802}
803
804impl FromStr for DiskCli {
805 type Err = anyhow::Error;
806
807 fn from_str(s: &str) -> anyhow::Result<Self> {
808 let mut opts = s.split(',');
809 let kind = opts.next().unwrap().parse()?;
810
811 let mut read_only = false;
812 let mut is_dvd = false;
813 let mut underhill = None;
814 let mut vtl = DeviceVtl::Vtl0;
815 for opt in opts {
816 let mut s = opt.split('=');
817 let opt = s.next().unwrap();
818 match opt {
819 "ro" => read_only = true,
820 "dvd" => {
821 is_dvd = true;
822 read_only = true;
823 }
824 "vtl2" => {
825 vtl = DeviceVtl::Vtl2;
826 }
827 "uh" => underhill = Some(UnderhillDiskSource::Scsi),
828 "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
829 opt => anyhow::bail!("unknown option: '{opt}'"),
830 }
831 }
832
833 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
834 anyhow::bail!("`uh` is incompatible with `vtl2`");
835 }
836
837 Ok(DiskCli {
838 vtl,
839 kind,
840 read_only,
841 is_dvd,
842 underhill,
843 })
844 }
845}
846
847#[derive(Clone)]
849pub struct IdeDiskCli {
850 pub kind: DiskCliKind,
851 pub read_only: bool,
852 pub channel: Option<u8>,
853 pub device: Option<u8>,
854 pub is_dvd: bool,
855}
856
857impl FromStr for IdeDiskCli {
858 type Err = anyhow::Error;
859
860 fn from_str(s: &str) -> anyhow::Result<Self> {
861 let mut opts = s.split(',');
862 let kind = opts.next().unwrap().parse()?;
863
864 let mut read_only = false;
865 let mut channel = None;
866 let mut device = None;
867 let mut is_dvd = false;
868 for opt in opts {
869 let mut s = opt.split('=');
870 let opt = s.next().unwrap();
871 match opt {
872 "ro" => read_only = true,
873 "p" => channel = Some(0),
874 "s" => channel = Some(1),
875 "0" => device = Some(0),
876 "1" => device = Some(1),
877 "dvd" => {
878 is_dvd = true;
879 read_only = true;
880 }
881 _ => anyhow::bail!("unknown option: '{opt}'"),
882 }
883 }
884
885 Ok(IdeDiskCli {
886 kind,
887 read_only,
888 channel,
889 device,
890 is_dvd,
891 })
892 }
893}
894
895#[derive(Clone)]
897pub struct FloppyDiskCli {
898 pub kind: DiskCliKind,
899 pub read_only: bool,
900}
901
902impl FromStr for FloppyDiskCli {
903 type Err = anyhow::Error;
904
905 fn from_str(s: &str) -> anyhow::Result<Self> {
906 let mut opts = s.split(',');
907 let kind = opts.next().unwrap().parse()?;
908
909 let mut read_only = false;
910 for opt in opts {
911 let mut s = opt.split('=');
912 let opt = s.next().unwrap();
913 match opt {
914 "ro" => read_only = true,
915 _ => anyhow::bail!("unknown option: '{opt}'"),
916 }
917 }
918
919 Ok(FloppyDiskCli { kind, read_only })
920 }
921}
922
923#[derive(Clone)]
924pub struct DebugconSerialConfigCli {
925 pub port: u16,
926 pub serial: SerialConfigCli,
927}
928
929impl FromStr for DebugconSerialConfigCli {
930 type Err = String;
931
932 fn from_str(s: &str) -> Result<Self, Self::Err> {
933 let Some((port, serial)) = s.split_once(',') else {
934 return Err("invalid format (missing comma between port and serial)".into());
935 };
936
937 let port: u16 = parse_number(port)
938 .map_err(|_| "could not parse port".to_owned())?
939 .try_into()
940 .map_err(|_| "port must be 16-bit")?;
941 let serial: SerialConfigCli = serial.parse()?;
942
943 Ok(Self { port, serial })
944 }
945}
946
947#[derive(Clone)]
949pub enum SerialConfigCli {
950 None,
951 Console,
952 NewConsole(Option<PathBuf>, Option<String>),
953 Stderr,
954 Pipe(PathBuf),
955 Tcp(SocketAddr),
956}
957
958impl FromStr for SerialConfigCli {
959 type Err = String;
960
961 fn from_str(s: &str) -> Result<Self, Self::Err> {
962 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
963
964 let first_key = match keyvalues.first() {
965 Some(first_pair) => first_pair.0.as_str(),
966 None => Err("invalid serial configuration: no values supplied")?,
967 };
968 let first_value = keyvalues.first().unwrap().1.as_ref();
969
970 let ret = match first_key {
971 "none" => SerialConfigCli::None,
972 "console" => SerialConfigCli::Console,
973 "stderr" => SerialConfigCli::Stderr,
974 "term" => match first_value {
975 Some(path) => {
976 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
978 let window_name = match window_name {
979 Some((_, Some(name))) => Some(name.clone()),
980 _ => None,
981 };
982
983 SerialConfigCli::NewConsole(Some(path.into()), window_name)
984 }
985 None => SerialConfigCli::NewConsole(None, None),
986 },
987 "listen" => match first_value {
988 Some(path) => {
989 if let Some(tcp) = path.strip_prefix("tcp:") {
990 let addr = tcp
991 .parse()
992 .map_err(|err| format!("invalid tcp address: {err}"))?;
993 SerialConfigCli::Tcp(addr)
994 } else {
995 SerialConfigCli::Pipe(s.into())
996 }
997 }
998 None => Err(
999 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1000 )?,
1001 },
1002 _ => {
1003 return Err(format!(
1004 "invalid serial configuration: '{}' is not a known option",
1005 first_key
1006 ));
1007 }
1008 };
1009
1010 Ok(ret)
1011 }
1012}
1013
1014impl SerialConfigCli {
1015 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1018 let mut ret = Vec::new();
1019
1020 for item in s.split(',') {
1022 let mut eqsplit = item.split('=');
1025 let key = eqsplit.next();
1026 let value = eqsplit.next();
1027
1028 if let Some(key) = key {
1029 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1030 } else {
1031 return Err("invalid key=value pair in serial config".into());
1033 }
1034 }
1035 Ok(ret)
1036 }
1037}
1038
1039#[derive(Clone)]
1040pub enum EndpointConfigCli {
1041 None,
1042 Consomme { cidr: Option<String> },
1043 Dio { id: Option<String> },
1044 Tap { name: String },
1045}
1046
1047impl FromStr for EndpointConfigCli {
1048 type Err = String;
1049
1050 fn from_str(s: &str) -> Result<Self, Self::Err> {
1051 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1052 ["none"] => EndpointConfigCli::None,
1053 ["consomme", s @ ..] => EndpointConfigCli::Consomme {
1054 cidr: s.first().map(|&s| s.to_owned()),
1055 },
1056 ["dio", s @ ..] => EndpointConfigCli::Dio {
1057 id: s.first().map(|s| (*s).to_owned()),
1058 },
1059 ["tap", name] => EndpointConfigCli::Tap {
1060 name: (*name).to_owned(),
1061 },
1062 _ => return Err("invalid network backend".into()),
1063 };
1064
1065 Ok(ret)
1066 }
1067}
1068
1069#[derive(Clone)]
1070pub struct NicConfigCli {
1071 pub vtl: DeviceVtl,
1072 pub endpoint: EndpointConfigCli,
1073 pub max_queues: Option<u16>,
1074 pub underhill: bool,
1075}
1076
1077impl FromStr for NicConfigCli {
1078 type Err = String;
1079
1080 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1081 let mut vtl = DeviceVtl::Vtl0;
1082 let mut max_queues = None;
1083 let mut underhill = false;
1084 while let Some((opt, rest)) = s.split_once(':') {
1085 if let Some((opt, val)) = opt.split_once('=') {
1086 match opt {
1087 "queues" => {
1088 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1089 }
1090 _ => break,
1091 }
1092 } else {
1093 match opt {
1094 "vtl2" => {
1095 vtl = DeviceVtl::Vtl2;
1096 }
1097 "uh" => underhill = true,
1098 _ => break,
1099 }
1100 }
1101 s = rest;
1102 }
1103
1104 if underhill && vtl != DeviceVtl::Vtl0 {
1105 return Err("`uh` is incompatible with `vtl2`".into());
1106 }
1107
1108 let endpoint = s.parse()?;
1109 Ok(NicConfigCli {
1110 vtl,
1111 endpoint,
1112 max_queues,
1113 underhill,
1114 })
1115 }
1116}
1117
1118#[derive(Debug, Error)]
1119#[error("unknown hypervisor: {0}")]
1120pub struct UnknownHypervisor(String);
1121
1122fn parse_hypervisor(s: &str) -> Result<Hypervisor, UnknownHypervisor> {
1123 match s {
1124 "kvm" => Ok(Hypervisor::Kvm),
1125 "mshv" => Ok(Hypervisor::MsHv),
1126 "whp" => Ok(Hypervisor::Whp),
1127 _ => Err(UnknownHypervisor(s.to_owned())),
1128 }
1129}
1130
1131#[derive(Debug, Error)]
1132#[error("unknown VTL2 relocation type: {0}")]
1133pub struct UnknownVtl2RelocationType(String);
1134
1135fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1136 match s {
1137 "disable" => Ok(Vtl2BaseAddressType::File),
1138 s if s.starts_with("auto=") => {
1139 let s = s.strip_prefix("auto=").unwrap_or_default();
1140 let size = if s == "filesize" {
1141 None
1142 } else {
1143 let size = parse_memory(s).map_err(|e| {
1144 UnknownVtl2RelocationType(format!(
1145 "unable to parse memory size from {} for 'auto=' type, {e}",
1146 e
1147 ))
1148 })?;
1149 Some(size)
1150 };
1151 Ok(Vtl2BaseAddressType::MemoryLayout { size })
1152 }
1153 s if s.starts_with("absolute=") => {
1154 let s = s.strip_prefix("absolute=");
1155 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
1156 UnknownVtl2RelocationType(format!(
1157 "unable to parse number from {} for 'absolute=' type",
1158 e
1159 ))
1160 })?;
1161 Ok(Vtl2BaseAddressType::Absolute(addr))
1162 }
1163 s if s.starts_with("vtl2=") => {
1164 let s = s.strip_prefix("vtl2=").unwrap_or_default();
1165 let size = if s == "filesize" {
1166 None
1167 } else {
1168 let size = parse_memory(s).map_err(|e| {
1169 UnknownVtl2RelocationType(format!(
1170 "unable to parse memory size from {} for 'vtl2=' type, {e}",
1171 e
1172 ))
1173 })?;
1174 Some(size)
1175 };
1176 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
1177 }
1178 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
1179 }
1180}
1181
1182#[derive(Debug, Copy, Clone)]
1183pub enum SmtConfigCli {
1184 Auto,
1185 Force,
1186 Off,
1187}
1188
1189#[derive(Debug, Error)]
1190#[error("expected auto, force, or off")]
1191pub struct BadSmtConfig;
1192
1193impl FromStr for SmtConfigCli {
1194 type Err = BadSmtConfig;
1195
1196 fn from_str(s: &str) -> Result<Self, Self::Err> {
1197 let r = match s {
1198 "auto" => Self::Auto,
1199 "force" => Self::Force,
1200 "off" => Self::Off,
1201 _ => return Err(BadSmtConfig),
1202 };
1203 Ok(r)
1204 }
1205}
1206
1207#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
1208fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
1209 let r = match s {
1210 "auto" => X2ApicConfig::Auto,
1211 "supported" => X2ApicConfig::Supported,
1212 "off" => X2ApicConfig::Unsupported,
1213 "on" => X2ApicConfig::Enabled,
1214 _ => return Err("expected auto, supported, off, or on"),
1215 };
1216 Ok(r)
1217}
1218
1219#[derive(Debug, Copy, Clone, ValueEnum)]
1220pub enum Vtl0LateMapPolicyCli {
1221 Off,
1222 Log,
1223 Halt,
1224 Exception,
1225}
1226
1227#[derive(Debug, Copy, Clone, ValueEnum)]
1228pub enum IsolationCli {
1229 Vbs,
1230}
1231
1232#[derive(Debug, Copy, Clone)]
1233pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
1234
1235impl FromStr for PcatBootOrderCli {
1236 type Err = &'static str;
1237
1238 fn from_str(s: &str) -> Result<Self, Self::Err> {
1239 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
1240 let mut order = Vec::new();
1241
1242 for item in s.split(',') {
1243 let device = match item {
1244 "optical" => PcatBootDevice::Optical,
1245 "hdd" => PcatBootDevice::HardDrive,
1246 "net" => PcatBootDevice::Network,
1247 "floppy" => PcatBootDevice::Floppy,
1248 _ => return Err("unknown boot device type"),
1249 };
1250
1251 let default_pos = default_order
1252 .iter()
1253 .position(|x| x == &Some(device))
1254 .ok_or("cannot pass duplicate boot devices")?;
1255
1256 order.push(default_order[default_pos].take().unwrap());
1257 }
1258
1259 order.extend(default_order.into_iter().flatten());
1260 assert_eq!(order.len(), 4);
1261
1262 Ok(Self(order.try_into().unwrap()))
1263 }
1264}
1265
1266#[derive(Copy, Clone, Debug, ValueEnum)]
1267pub enum UefiConsoleModeCli {
1268 Default,
1269 Com1,
1270 Com2,
1271 None,
1272}
1273
1274fn default_value_from_arch_env(name: &str) -> OsString {
1282 let prefix = if cfg!(guest_arch = "x86_64") {
1283 "X86_64"
1284 } else if cfg!(guest_arch = "aarch64") {
1285 "AARCH64"
1286 } else {
1287 return Default::default();
1288 };
1289 let prefixed = format!("{}_{}", prefix, name);
1290 std::env::var_os(name)
1291 .or_else(|| std::env::var_os(prefixed))
1292 .unwrap_or_default()
1293}
1294
1295#[derive(Clone)]
1297pub struct OptionalPathBuf(pub Option<PathBuf>);
1298
1299impl From<&std::ffi::OsStr> for OptionalPathBuf {
1300 fn from(s: &std::ffi::OsStr) -> Self {
1301 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
1302 }
1303}