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` or `VMGS_DEFAULT`
400 `memdiff:<disk>[;create=<len>]` 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, Debug, PartialEq)]
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, Debug, PartialEq)]
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 if s == "VMGS_DEFAULT" {
615 Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
616 } else {
617 || -> Option<u64> {
618 let mut b = s.as_bytes();
619 if s.ends_with('B') {
620 b = &b[..b.len() - 1]
621 }
622 if b.is_empty() {
623 return None;
624 }
625 let multi = match b[b.len() - 1] as char {
626 'T' => Some(1024 * 1024 * 1024 * 1024),
627 'G' => Some(1024 * 1024 * 1024),
628 'M' => Some(1024 * 1024),
629 'K' => Some(1024),
630 _ => None,
631 };
632 if multi.is_some() {
633 b = &b[..b.len() - 1]
634 }
635 let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
636 Some(n * multi.unwrap_or(1))
637 }()
638 .with_context(|| format!("invalid memory size '{0}'", s))
639 }
640}
641
642fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
644 match s.strip_prefix("0x") {
645 Some(rest) => u64::from_str_radix(rest, 16),
646 None => s.parse::<u64>(),
647 }
648}
649
650#[derive(Clone, Debug, PartialEq)]
651pub enum DiskCliKind {
652 Memory(u64),
654 MemoryDiff(Box<DiskCliKind>),
656 Sqlite {
658 path: PathBuf,
659 create_with_len: Option<u64>,
660 },
661 SqliteDiff {
663 path: PathBuf,
664 create: bool,
665 disk: Box<DiskCliKind>,
666 },
667 AutoCacheSqlite {
669 cache_path: String,
670 key: Option<String>,
671 disk: Box<DiskCliKind>,
672 },
673 PersistentReservationsWrapper(Box<DiskCliKind>),
675 File {
677 path: PathBuf,
678 create_with_len: Option<u64>,
679 },
680 Blob {
682 kind: BlobKind,
683 url: String,
684 },
685 Crypt {
687 cipher: DiskCipher,
688 key_file: PathBuf,
689 disk: Box<DiskCliKind>,
690 },
691 DelayDiskWrapper {
693 delay_ms: u64,
694 disk: Box<DiskCliKind>,
695 },
696}
697
698#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
699pub enum DiskCipher {
700 #[clap(name = "xts-aes-256")]
701 XtsAes256,
702}
703
704#[derive(Copy, Clone, Debug, PartialEq)]
705pub enum BlobKind {
706 Flat,
707 Vhd1,
708}
709
710fn parse_path_and_len(arg: &str) -> anyhow::Result<(PathBuf, Option<u64>)> {
711 Ok(match arg.split_once(';') {
712 Some((path, len)) => {
713 let Some(len) = len.strip_prefix("create=") else {
714 anyhow::bail!("invalid syntax after ';', expected 'create=<len>'")
715 };
716
717 let len = parse_memory(len)?;
718
719 (path.into(), Some(len))
720 }
721 None => (arg.into(), None),
722 })
723}
724
725impl FromStr for DiskCliKind {
726 type Err = anyhow::Error;
727
728 fn from_str(s: &str) -> anyhow::Result<Self> {
729 let disk = match s.split_once(':') {
730 None => {
732 let (path, create_with_len) = parse_path_and_len(s)?;
733 DiskCliKind::File {
734 path,
735 create_with_len,
736 }
737 }
738 Some((kind, arg)) => match kind {
739 "mem" => DiskCliKind::Memory(parse_memory(arg)?),
740 "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
741 "sql" => {
742 let (path, create_with_len) = parse_path_and_len(arg)?;
743 DiskCliKind::Sqlite {
744 path,
745 create_with_len,
746 }
747 }
748 "sqldiff" => {
749 let (path_and_opts, kind) =
750 arg.split_once(':').context("expected path[;opts]:kind")?;
751 let disk = Box::new(kind.parse()?);
752 match path_and_opts.split_once(';') {
753 Some((path, create)) => {
754 if create != "create" {
755 anyhow::bail!("invalid syntax after ';', expected 'create'")
756 }
757 DiskCliKind::SqliteDiff {
758 path: path.into(),
759 create: true,
760 disk,
761 }
762 }
763 None => DiskCliKind::SqliteDiff {
764 path: path_and_opts.into(),
765 create: false,
766 disk,
767 },
768 }
769 }
770 "autocache" => {
771 let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
772 let cache_path = std::env::var("OPENVMM_AUTO_CACHE_PATH")
773 .context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
774 DiskCliKind::AutoCacheSqlite {
775 cache_path,
776 key: (!key.is_empty()).then(|| key.to_string()),
777 disk: Box::new(kind.parse()?),
778 }
779 }
780 "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
781 "file" => {
782 let (path, create_with_len) = parse_path_and_len(arg)?;
783 DiskCliKind::File {
784 path,
785 create_with_len,
786 }
787 }
788 "blob" => {
789 let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
790 let blob_kind = match blob_kind {
791 "flat" => BlobKind::Flat,
792 "vhd1" => BlobKind::Vhd1,
793 _ => anyhow::bail!("unknown blob kind {blob_kind}"),
794 };
795 DiskCliKind::Blob {
796 kind: blob_kind,
797 url: url.to_string(),
798 }
799 }
800 "crypt" => {
801 let (cipher, (key, kind)) = arg
802 .split_once(':')
803 .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
804 .context("expected cipher:key_file:kind")?;
805 DiskCliKind::Crypt {
806 cipher: ValueEnum::from_str(cipher, false)
807 .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
808 key_file: PathBuf::from(key),
809 disk: Box::new(kind.parse()?),
810 }
811 }
812 kind => {
813 let (path, create_with_len) = parse_path_and_len(s)?;
818 if path.has_root() {
819 DiskCliKind::File {
820 path,
821 create_with_len,
822 }
823 } else {
824 anyhow::bail!("invalid disk kind {kind}");
825 }
826 }
827 },
828 };
829 Ok(disk)
830 }
831}
832
833#[derive(Clone)]
834pub struct VmgsCli {
835 pub kind: DiskCliKind,
836 pub provision: ProvisionVmgs,
837}
838
839#[derive(Copy, Clone)]
840pub enum ProvisionVmgs {
841 OnEmpty,
842 OnFailure,
843 True,
844}
845
846impl FromStr for VmgsCli {
847 type Err = anyhow::Error;
848
849 fn from_str(s: &str) -> anyhow::Result<Self> {
850 let (kind, opt) = s
851 .split_once(',')
852 .map(|(k, o)| (k, Some(o)))
853 .unwrap_or((s, None));
854 let kind = kind.parse()?;
855
856 let provision = match opt {
857 None => ProvisionVmgs::OnEmpty,
858 Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
859 Some("fmt") => ProvisionVmgs::True,
860 Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
861 };
862
863 Ok(VmgsCli { kind, provision })
864 }
865}
866
867#[derive(Clone)]
869pub struct DiskCli {
870 pub vtl: DeviceVtl,
871 pub kind: DiskCliKind,
872 pub read_only: bool,
873 pub is_dvd: bool,
874 pub underhill: Option<UnderhillDiskSource>,
875}
876
877#[derive(Copy, Clone)]
878pub enum UnderhillDiskSource {
879 Scsi,
880 Nvme,
881}
882
883impl FromStr for DiskCli {
884 type Err = anyhow::Error;
885
886 fn from_str(s: &str) -> anyhow::Result<Self> {
887 let mut opts = s.split(',');
888 let kind = opts.next().unwrap().parse()?;
889
890 let mut read_only = false;
891 let mut is_dvd = false;
892 let mut underhill = None;
893 let mut vtl = DeviceVtl::Vtl0;
894 for opt in opts {
895 let mut s = opt.split('=');
896 let opt = s.next().unwrap();
897 match opt {
898 "ro" => read_only = true,
899 "dvd" => {
900 is_dvd = true;
901 read_only = true;
902 }
903 "vtl2" => {
904 vtl = DeviceVtl::Vtl2;
905 }
906 "uh" => underhill = Some(UnderhillDiskSource::Scsi),
907 "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
908 opt => anyhow::bail!("unknown option: '{opt}'"),
909 }
910 }
911
912 if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
913 anyhow::bail!("`uh` is incompatible with `vtl2`");
914 }
915
916 Ok(DiskCli {
917 vtl,
918 kind,
919 read_only,
920 is_dvd,
921 underhill,
922 })
923 }
924}
925
926#[derive(Clone)]
928pub struct IdeDiskCli {
929 pub kind: DiskCliKind,
930 pub read_only: bool,
931 pub channel: Option<u8>,
932 pub device: Option<u8>,
933 pub is_dvd: bool,
934}
935
936impl FromStr for IdeDiskCli {
937 type Err = anyhow::Error;
938
939 fn from_str(s: &str) -> anyhow::Result<Self> {
940 let mut opts = s.split(',');
941 let kind = opts.next().unwrap().parse()?;
942
943 let mut read_only = false;
944 let mut channel = None;
945 let mut device = None;
946 let mut is_dvd = false;
947 for opt in opts {
948 let mut s = opt.split('=');
949 let opt = s.next().unwrap();
950 match opt {
951 "ro" => read_only = true,
952 "p" => channel = Some(0),
953 "s" => channel = Some(1),
954 "0" => device = Some(0),
955 "1" => device = Some(1),
956 "dvd" => {
957 is_dvd = true;
958 read_only = true;
959 }
960 _ => anyhow::bail!("unknown option: '{opt}'"),
961 }
962 }
963
964 Ok(IdeDiskCli {
965 kind,
966 read_only,
967 channel,
968 device,
969 is_dvd,
970 })
971 }
972}
973
974#[derive(Clone, Debug, PartialEq)]
976pub struct FloppyDiskCli {
977 pub kind: DiskCliKind,
978 pub read_only: bool,
979}
980
981impl FromStr for FloppyDiskCli {
982 type Err = anyhow::Error;
983
984 fn from_str(s: &str) -> anyhow::Result<Self> {
985 if s.is_empty() {
986 anyhow::bail!("empty disk spec");
987 }
988 let mut opts = s.split(',');
989 let kind = opts.next().unwrap().parse()?;
990
991 let mut read_only = false;
992 for opt in opts {
993 let mut s = opt.split('=');
994 let opt = s.next().unwrap();
995 match opt {
996 "ro" => read_only = true,
997 _ => anyhow::bail!("unknown option: '{opt}'"),
998 }
999 }
1000
1001 Ok(FloppyDiskCli { kind, read_only })
1002 }
1003}
1004
1005#[derive(Clone)]
1006pub struct DebugconSerialConfigCli {
1007 pub port: u16,
1008 pub serial: SerialConfigCli,
1009}
1010
1011impl FromStr for DebugconSerialConfigCli {
1012 type Err = String;
1013
1014 fn from_str(s: &str) -> Result<Self, Self::Err> {
1015 let Some((port, serial)) = s.split_once(',') else {
1016 return Err("invalid format (missing comma between port and serial)".into());
1017 };
1018
1019 let port: u16 = parse_number(port)
1020 .map_err(|_| "could not parse port".to_owned())?
1021 .try_into()
1022 .map_err(|_| "port must be 16-bit")?;
1023 let serial: SerialConfigCli = serial.parse()?;
1024
1025 Ok(Self { port, serial })
1026 }
1027}
1028
1029#[derive(Clone, Debug, PartialEq)]
1031pub enum SerialConfigCli {
1032 None,
1033 Console,
1034 NewConsole(Option<PathBuf>, Option<String>),
1035 Stderr,
1036 Pipe(PathBuf),
1037 Tcp(SocketAddr),
1038 File(PathBuf),
1039}
1040
1041impl FromStr for SerialConfigCli {
1042 type Err = String;
1043
1044 fn from_str(s: &str) -> Result<Self, Self::Err> {
1045 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1046
1047 let first_key = match keyvalues.first() {
1048 Some(first_pair) => first_pair.0.as_str(),
1049 None => Err("invalid serial configuration: no values supplied")?,
1050 };
1051 let first_value = keyvalues.first().unwrap().1.as_ref();
1052
1053 let ret = match first_key {
1054 "none" => SerialConfigCli::None,
1055 "console" => SerialConfigCli::Console,
1056 "stderr" => SerialConfigCli::Stderr,
1057 "file" => match first_value {
1058 Some(path) => SerialConfigCli::File(path.into()),
1059 None => Err("invalid serial configuration: file requires a value")?,
1060 },
1061 "term" => match first_value {
1062 Some(path) => {
1063 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1065 let window_name = match window_name {
1066 Some((_, Some(name))) => Some(name.clone()),
1067 _ => None,
1068 };
1069
1070 SerialConfigCli::NewConsole(Some(path.into()), window_name)
1071 }
1072 None => SerialConfigCli::NewConsole(None, None),
1073 },
1074 "listen" => match first_value {
1075 Some(path) => {
1076 if let Some(tcp) = path.strip_prefix("tcp:") {
1077 let addr = tcp
1078 .parse()
1079 .map_err(|err| format!("invalid tcp address: {err}"))?;
1080 SerialConfigCli::Tcp(addr)
1081 } else {
1082 SerialConfigCli::Pipe(path.into())
1083 }
1084 }
1085 None => Err(
1086 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1087 )?,
1088 },
1089 _ => {
1090 return Err(format!(
1091 "invalid serial configuration: '{}' is not a known option",
1092 first_key
1093 ));
1094 }
1095 };
1096
1097 Ok(ret)
1098 }
1099}
1100
1101impl SerialConfigCli {
1102 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1105 let mut ret = Vec::new();
1106
1107 for item in s.split(',') {
1109 let mut eqsplit = item.split('=');
1112 let key = eqsplit.next();
1113 let value = eqsplit.next();
1114
1115 if let Some(key) = key {
1116 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1117 } else {
1118 return Err("invalid key=value pair in serial config".into());
1120 }
1121 }
1122 Ok(ret)
1123 }
1124}
1125
1126#[derive(Clone, Debug, PartialEq)]
1127pub enum EndpointConfigCli {
1128 None,
1129 Consomme { cidr: Option<String> },
1130 Dio { id: Option<String> },
1131 Tap { name: String },
1132}
1133
1134impl FromStr for EndpointConfigCli {
1135 type Err = String;
1136
1137 fn from_str(s: &str) -> Result<Self, Self::Err> {
1138 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1139 ["none"] => EndpointConfigCli::None,
1140 ["consomme", s @ ..] => EndpointConfigCli::Consomme {
1141 cidr: s.first().map(|&s| s.to_owned()),
1142 },
1143 ["dio", s @ ..] => EndpointConfigCli::Dio {
1144 id: s.first().map(|s| (*s).to_owned()),
1145 },
1146 ["tap", name] => EndpointConfigCli::Tap {
1147 name: (*name).to_owned(),
1148 },
1149 _ => return Err("invalid network backend".into()),
1150 };
1151
1152 Ok(ret)
1153 }
1154}
1155
1156#[derive(Clone, Debug, PartialEq)]
1157pub struct NicConfigCli {
1158 pub vtl: DeviceVtl,
1159 pub endpoint: EndpointConfigCli,
1160 pub max_queues: Option<u16>,
1161 pub underhill: bool,
1162}
1163
1164impl FromStr for NicConfigCli {
1165 type Err = String;
1166
1167 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1168 let mut vtl = DeviceVtl::Vtl0;
1169 let mut max_queues = None;
1170 let mut underhill = false;
1171 while let Some((opt, rest)) = s.split_once(':') {
1172 if let Some((opt, val)) = opt.split_once('=') {
1173 match opt {
1174 "queues" => {
1175 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1176 }
1177 _ => break,
1178 }
1179 } else {
1180 match opt {
1181 "vtl2" => {
1182 vtl = DeviceVtl::Vtl2;
1183 }
1184 "uh" => underhill = true,
1185 _ => break,
1186 }
1187 }
1188 s = rest;
1189 }
1190
1191 if underhill && vtl != DeviceVtl::Vtl0 {
1192 return Err("`uh` is incompatible with `vtl2`".into());
1193 }
1194
1195 let endpoint = s.parse()?;
1196 Ok(NicConfigCli {
1197 vtl,
1198 endpoint,
1199 max_queues,
1200 underhill,
1201 })
1202 }
1203}
1204
1205#[derive(Debug, Error)]
1206#[error("unknown hypervisor: {0}")]
1207pub struct UnknownHypervisor(String);
1208
1209fn parse_hypervisor(s: &str) -> Result<Hypervisor, UnknownHypervisor> {
1210 match s {
1211 "kvm" => Ok(Hypervisor::Kvm),
1212 "mshv" => Ok(Hypervisor::MsHv),
1213 "whp" => Ok(Hypervisor::Whp),
1214 _ => Err(UnknownHypervisor(s.to_owned())),
1215 }
1216}
1217
1218#[derive(Debug, Error)]
1219#[error("unknown VTL2 relocation type: {0}")]
1220pub struct UnknownVtl2RelocationType(String);
1221
1222fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1223 match s {
1224 "disable" => Ok(Vtl2BaseAddressType::File),
1225 s if s.starts_with("auto=") => {
1226 let s = s.strip_prefix("auto=").unwrap_or_default();
1227 let size = if s == "filesize" {
1228 None
1229 } else {
1230 let size = parse_memory(s).map_err(|e| {
1231 UnknownVtl2RelocationType(format!(
1232 "unable to parse memory size from {} for 'auto=' type, {e}",
1233 e
1234 ))
1235 })?;
1236 Some(size)
1237 };
1238 Ok(Vtl2BaseAddressType::MemoryLayout { size })
1239 }
1240 s if s.starts_with("absolute=") => {
1241 let s = s.strip_prefix("absolute=");
1242 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
1243 UnknownVtl2RelocationType(format!(
1244 "unable to parse number from {} for 'absolute=' type",
1245 e
1246 ))
1247 })?;
1248 Ok(Vtl2BaseAddressType::Absolute(addr))
1249 }
1250 s if s.starts_with("vtl2=") => {
1251 let s = s.strip_prefix("vtl2=").unwrap_or_default();
1252 let size = if s == "filesize" {
1253 None
1254 } else {
1255 let size = parse_memory(s).map_err(|e| {
1256 UnknownVtl2RelocationType(format!(
1257 "unable to parse memory size from {} for 'vtl2=' type, {e}",
1258 e
1259 ))
1260 })?;
1261 Some(size)
1262 };
1263 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
1264 }
1265 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
1266 }
1267}
1268
1269#[derive(Debug, Copy, Clone, PartialEq)]
1270pub enum SmtConfigCli {
1271 Auto,
1272 Force,
1273 Off,
1274}
1275
1276#[derive(Debug, Error)]
1277#[error("expected auto, force, or off")]
1278pub struct BadSmtConfig;
1279
1280impl FromStr for SmtConfigCli {
1281 type Err = BadSmtConfig;
1282
1283 fn from_str(s: &str) -> Result<Self, Self::Err> {
1284 let r = match s {
1285 "auto" => Self::Auto,
1286 "force" => Self::Force,
1287 "off" => Self::Off,
1288 _ => return Err(BadSmtConfig),
1289 };
1290 Ok(r)
1291 }
1292}
1293
1294#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
1295fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
1296 let r = match s {
1297 "auto" => X2ApicConfig::Auto,
1298 "supported" => X2ApicConfig::Supported,
1299 "off" => X2ApicConfig::Unsupported,
1300 "on" => X2ApicConfig::Enabled,
1301 _ => return Err("expected auto, supported, off, or on"),
1302 };
1303 Ok(r)
1304}
1305
1306#[derive(Debug, Copy, Clone, ValueEnum)]
1307pub enum Vtl0LateMapPolicyCli {
1308 Off,
1309 Log,
1310 Halt,
1311 Exception,
1312}
1313
1314#[derive(Debug, Copy, Clone, ValueEnum)]
1315pub enum IsolationCli {
1316 Vbs,
1317}
1318
1319#[derive(Debug, Copy, Clone, PartialEq)]
1320pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
1321
1322impl FromStr for PcatBootOrderCli {
1323 type Err = &'static str;
1324
1325 fn from_str(s: &str) -> Result<Self, Self::Err> {
1326 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
1327 let mut order = Vec::new();
1328
1329 for item in s.split(',') {
1330 let device = match item {
1331 "optical" => PcatBootDevice::Optical,
1332 "hdd" => PcatBootDevice::HardDrive,
1333 "net" => PcatBootDevice::Network,
1334 "floppy" => PcatBootDevice::Floppy,
1335 _ => return Err("unknown boot device type"),
1336 };
1337
1338 let default_pos = default_order
1339 .iter()
1340 .position(|x| x == &Some(device))
1341 .ok_or("cannot pass duplicate boot devices")?;
1342
1343 order.push(default_order[default_pos].take().unwrap());
1344 }
1345
1346 order.extend(default_order.into_iter().flatten());
1347 assert_eq!(order.len(), 4);
1348
1349 Ok(Self(order.try_into().unwrap()))
1350 }
1351}
1352
1353#[derive(Copy, Clone, Debug, ValueEnum)]
1354pub enum UefiConsoleModeCli {
1355 Default,
1356 Com1,
1357 Com2,
1358 None,
1359}
1360
1361fn default_value_from_arch_env(name: &str) -> OsString {
1369 let prefix = if cfg!(guest_arch = "x86_64") {
1370 "X86_64"
1371 } else if cfg!(guest_arch = "aarch64") {
1372 "AARCH64"
1373 } else {
1374 return Default::default();
1375 };
1376 let prefixed = format!("{}_{}", prefix, name);
1377 std::env::var_os(name)
1378 .or_else(|| std::env::var_os(prefixed))
1379 .unwrap_or_default()
1380}
1381
1382#[derive(Clone)]
1384pub struct OptionalPathBuf(pub Option<PathBuf>);
1385
1386impl From<&std::ffi::OsStr> for OptionalPathBuf {
1387 fn from(s: &std::ffi::OsStr) -> Self {
1388 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
1389 }
1390}
1391
1392#[cfg(test)]
1393#[expect(unsafe_code)]
1395mod tests {
1396 use super::*;
1397
1398 fn with_env_var<F, R>(name: &str, value: &str, f: F) -> R
1399 where
1400 F: FnOnce() -> R,
1401 {
1402 unsafe {
1405 std::env::set_var(name, value);
1406 }
1407 let result = f();
1408 unsafe {
1411 std::env::remove_var(name);
1412 }
1413 result
1414 }
1415
1416 #[test]
1417 fn test_parse_file_disk_with_create() {
1418 let s = "file:test.vhd;create=1G";
1419 let disk = DiskCliKind::from_str(s).unwrap();
1420
1421 match disk {
1422 DiskCliKind::File {
1423 path,
1424 create_with_len,
1425 } => {
1426 assert_eq!(path, PathBuf::from("test.vhd"));
1427 assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); }
1429 _ => panic!("Expected File variant"),
1430 }
1431 }
1432
1433 #[test]
1434 fn test_parse_direct_file_with_create() {
1435 let s = "test.vhd;create=1G";
1436 let disk = DiskCliKind::from_str(s).unwrap();
1437
1438 match disk {
1439 DiskCliKind::File {
1440 path,
1441 create_with_len,
1442 } => {
1443 assert_eq!(path, PathBuf::from("test.vhd"));
1444 assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); }
1446 _ => panic!("Expected File variant"),
1447 }
1448 }
1449
1450 #[test]
1451 fn test_parse_memory_disk() {
1452 let s = "mem:1G";
1453 let disk = DiskCliKind::from_str(s).unwrap();
1454 match disk {
1455 DiskCliKind::Memory(size) => {
1456 assert_eq!(size, 1024 * 1024 * 1024); }
1458 _ => panic!("Expected Memory variant"),
1459 }
1460 }
1461
1462 #[test]
1463 fn test_parse_memory_diff_disk() {
1464 let s = "memdiff:file:base.img";
1465 let disk = DiskCliKind::from_str(s).unwrap();
1466 match disk {
1467 DiskCliKind::MemoryDiff(inner) => match *inner {
1468 DiskCliKind::File {
1469 path,
1470 create_with_len,
1471 } => {
1472 assert_eq!(path, PathBuf::from("base.img"));
1473 assert_eq!(create_with_len, None);
1474 }
1475 _ => panic!("Expected File variant inside MemoryDiff"),
1476 },
1477 _ => panic!("Expected MemoryDiff variant"),
1478 }
1479 }
1480
1481 #[test]
1482 fn test_parse_sqlite_disk() {
1483 let s = "sql:db.sqlite;create=2G";
1484 let disk = DiskCliKind::from_str(s).unwrap();
1485 match disk {
1486 DiskCliKind::Sqlite {
1487 path,
1488 create_with_len,
1489 } => {
1490 assert_eq!(path, PathBuf::from("db.sqlite"));
1491 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
1492 }
1493 _ => panic!("Expected Sqlite variant"),
1494 }
1495
1496 let s = "sql:db.sqlite";
1498 let disk = DiskCliKind::from_str(s).unwrap();
1499 match disk {
1500 DiskCliKind::Sqlite {
1501 path,
1502 create_with_len,
1503 } => {
1504 assert_eq!(path, PathBuf::from("db.sqlite"));
1505 assert_eq!(create_with_len, None);
1506 }
1507 _ => panic!("Expected Sqlite variant"),
1508 }
1509 }
1510
1511 #[test]
1512 fn test_parse_sqlite_diff_disk() {
1513 let s = "sqldiff:diff.sqlite;create:file:base.img";
1515 let disk = DiskCliKind::from_str(s).unwrap();
1516 match disk {
1517 DiskCliKind::SqliteDiff { path, create, disk } => {
1518 assert_eq!(path, PathBuf::from("diff.sqlite"));
1519 assert!(create);
1520 match *disk {
1521 DiskCliKind::File {
1522 path,
1523 create_with_len,
1524 } => {
1525 assert_eq!(path, PathBuf::from("base.img"));
1526 assert_eq!(create_with_len, None);
1527 }
1528 _ => panic!("Expected File variant inside SqliteDiff"),
1529 }
1530 }
1531 _ => panic!("Expected SqliteDiff variant"),
1532 }
1533
1534 let s = "sqldiff:diff.sqlite:file:base.img";
1536 let disk = DiskCliKind::from_str(s).unwrap();
1537 match disk {
1538 DiskCliKind::SqliteDiff { path, create, disk } => {
1539 assert_eq!(path, PathBuf::from("diff.sqlite"));
1540 assert!(!create);
1541 match *disk {
1542 DiskCliKind::File {
1543 path,
1544 create_with_len,
1545 } => {
1546 assert_eq!(path, PathBuf::from("base.img"));
1547 assert_eq!(create_with_len, None);
1548 }
1549 _ => panic!("Expected File variant inside SqliteDiff"),
1550 }
1551 }
1552 _ => panic!("Expected SqliteDiff variant"),
1553 }
1554 }
1555
1556 #[test]
1557 fn test_parse_autocache_sqlite_disk() {
1558 let disk = with_env_var("OPENVMM_AUTO_CACHE_PATH", "/tmp/cache", || {
1560 DiskCliKind::from_str("autocache::file:disk.vhd").unwrap()
1561 });
1562 assert!(matches!(
1563 disk,
1564 DiskCliKind::AutoCacheSqlite {
1565 cache_path,
1566 key,
1567 disk: _disk,
1568 } if cache_path == "/tmp/cache" && key.is_none()
1569 ));
1570
1571 assert!(DiskCliKind::from_str("autocache::file:disk.vhd").is_err());
1573 }
1574
1575 #[test]
1576 fn test_parse_disk_errors() {
1577 assert!(DiskCliKind::from_str("invalid:").is_err());
1578 assert!(DiskCliKind::from_str("memory:extra").is_err());
1579
1580 assert!(DiskCliKind::from_str("sqlite:").is_err());
1582 }
1583
1584 #[test]
1585 fn test_parse_errors() {
1586 assert!(DiskCliKind::from_str("mem:invalid").is_err());
1588
1589 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
1591
1592 unsafe {
1596 std::env::remove_var("OPENVMM_AUTO_CACHE_PATH");
1597 }
1598 assert!(DiskCliKind::from_str("autocache:key:file:disk.vhd").is_err());
1599
1600 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
1602
1603 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
1605
1606 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
1608
1609 assert!(DiskCliKind::from_str("invalid:path").is_err());
1611
1612 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
1614 }
1615
1616 #[test]
1617 fn test_fs_args_from_str() {
1618 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
1619 assert_eq!(args.tag, "tag1");
1620 assert_eq!(args.path, "/path/to/fs");
1621
1622 assert!(FsArgs::from_str("tag1").is_err());
1624 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
1625 }
1626
1627 #[test]
1628 fn test_fs_args_with_options_from_str() {
1629 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
1630 assert_eq!(args.tag, "tag1");
1631 assert_eq!(args.path, "/path/to/fs");
1632 assert_eq!(args.options, "opt1;opt2");
1633
1634 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
1636 assert_eq!(args.tag, "tag1");
1637 assert_eq!(args.path, "/path/to/fs");
1638 assert_eq!(args.options, "");
1639
1640 assert!(FsArgsWithOptions::from_str("tag1").is_err());
1642 }
1643
1644 #[test]
1645 fn test_serial_config_from_str() {
1646 assert_eq!(
1647 SerialConfigCli::from_str("none").unwrap(),
1648 SerialConfigCli::None
1649 );
1650 assert_eq!(
1651 SerialConfigCli::from_str("console").unwrap(),
1652 SerialConfigCli::Console
1653 );
1654 assert_eq!(
1655 SerialConfigCli::from_str("stderr").unwrap(),
1656 SerialConfigCli::Stderr
1657 );
1658
1659 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
1661 if let SerialConfigCli::File(path) = file_config {
1662 assert_eq!(path.to_str().unwrap(), "/path/to/file");
1663 } else {
1664 panic!("Expected File variant");
1665 }
1666
1667 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
1669 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
1670 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
1671 assert_eq!(name, "MyTerm");
1672 }
1673 _ => panic!("Expected NewConsole variant with name"),
1674 }
1675
1676 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
1678 SerialConfigCli::NewConsole(Some(path), None) => {
1679 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
1680 }
1681 _ => panic!("Expected NewConsole variant without name"),
1682 }
1683
1684 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
1686 SerialConfigCli::Tcp(addr) => {
1687 assert_eq!(addr.to_string(), "127.0.0.1:1234");
1688 }
1689 _ => panic!("Expected Tcp variant"),
1690 }
1691
1692 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
1694 SerialConfigCli::Pipe(path) => {
1695 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
1696 }
1697 _ => panic!("Expected Pipe variant"),
1698 }
1699
1700 assert!(SerialConfigCli::from_str("").is_err());
1702 assert!(SerialConfigCli::from_str("unknown").is_err());
1703 assert!(SerialConfigCli::from_str("file").is_err());
1704 assert!(SerialConfigCli::from_str("listen").is_err());
1705 }
1706
1707 #[test]
1708 fn test_endpoint_config_from_str() {
1709 assert!(matches!(
1711 EndpointConfigCli::from_str("none").unwrap(),
1712 EndpointConfigCli::None
1713 ));
1714
1715 match EndpointConfigCli::from_str("consomme").unwrap() {
1717 EndpointConfigCli::Consomme { cidr: None } => (),
1718 _ => panic!("Expected Consomme variant without cidr"),
1719 }
1720
1721 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
1723 EndpointConfigCli::Consomme { cidr: Some(cidr) } => {
1724 assert_eq!(cidr, "192.168.0.0/24");
1725 }
1726 _ => panic!("Expected Consomme variant with cidr"),
1727 }
1728
1729 match EndpointConfigCli::from_str("dio").unwrap() {
1731 EndpointConfigCli::Dio { id: None } => (),
1732 _ => panic!("Expected Dio variant without id"),
1733 }
1734
1735 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
1737 EndpointConfigCli::Dio { id: Some(id) } => {
1738 assert_eq!(id, "test_id");
1739 }
1740 _ => panic!("Expected Dio variant with id"),
1741 }
1742
1743 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
1745 EndpointConfigCli::Tap { name } => {
1746 assert_eq!(name, "tap0");
1747 }
1748 _ => panic!("Expected Tap variant"),
1749 }
1750
1751 assert!(EndpointConfigCli::from_str("invalid").is_err());
1753 }
1754
1755 #[test]
1756 fn test_nic_config_from_str() {
1757 use hvlite_defs::config::DeviceVtl;
1758
1759 let config = NicConfigCli::from_str("none").unwrap();
1761 assert_eq!(config.vtl, DeviceVtl::Vtl0);
1762 assert!(config.max_queues.is_none());
1763 assert!(!config.underhill);
1764 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1765
1766 let config = NicConfigCli::from_str("vtl2:none").unwrap();
1768 assert_eq!(config.vtl, DeviceVtl::Vtl2);
1769 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1770
1771 let config = NicConfigCli::from_str("queues=4:none").unwrap();
1773 assert_eq!(config.max_queues, Some(4));
1774 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1775
1776 let config = NicConfigCli::from_str("uh:none").unwrap();
1778 assert!(config.underhill);
1779 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1780
1781 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
1783 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); }
1785
1786 #[test]
1787 fn test_smt_config_from_str() {
1788 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
1789 assert_eq!(
1790 SmtConfigCli::from_str("force").unwrap(),
1791 SmtConfigCli::Force
1792 );
1793 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
1794
1795 assert!(SmtConfigCli::from_str("invalid").is_err());
1797 assert!(SmtConfigCli::from_str("").is_err());
1798 }
1799
1800 #[test]
1801 fn test_pcat_boot_order_from_str() {
1802 let order = PcatBootOrderCli::from_str("optical").unwrap();
1804 assert_eq!(order.0[0], PcatBootDevice::Optical);
1805
1806 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
1808 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
1809 assert_eq!(order.0[1], PcatBootDevice::Network);
1810
1811 assert!(PcatBootOrderCli::from_str("invalid").is_err());
1813 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
1815
1816 #[test]
1817 fn test_floppy_disk_from_str() {
1818 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
1820 assert!(!disk.read_only);
1821 match disk.kind {
1822 DiskCliKind::File {
1823 path,
1824 create_with_len,
1825 } => {
1826 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
1827 assert_eq!(create_with_len, None);
1828 }
1829 _ => panic!("Expected File variant"),
1830 }
1831
1832 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
1834 assert!(disk.read_only);
1835
1836 assert!(FloppyDiskCli::from_str("").is_err());
1838 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
1839 }
1840}