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, 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 || -> 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, Debug, PartialEq)]
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, Debug, PartialEq)]
690pub enum DiskCipher {
691 #[clap(name = "xts-aes-256")]
692 XtsAes256,
693}
694
695#[derive(Copy, Clone, Debug, PartialEq)]
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(arg)?;
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, Debug, PartialEq)]
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 if s.is_empty() {
981 anyhow::bail!("empty disk spec");
982 }
983 let mut opts = s.split(',');
984 let kind = opts.next().unwrap().parse()?;
985
986 let mut read_only = false;
987 for opt in opts {
988 let mut s = opt.split('=');
989 let opt = s.next().unwrap();
990 match opt {
991 "ro" => read_only = true,
992 _ => anyhow::bail!("unknown option: '{opt}'"),
993 }
994 }
995
996 Ok(FloppyDiskCli { kind, read_only })
997 }
998}
999
1000#[derive(Clone)]
1001pub struct DebugconSerialConfigCli {
1002 pub port: u16,
1003 pub serial: SerialConfigCli,
1004}
1005
1006impl FromStr for DebugconSerialConfigCli {
1007 type Err = String;
1008
1009 fn from_str(s: &str) -> Result<Self, Self::Err> {
1010 let Some((port, serial)) = s.split_once(',') else {
1011 return Err("invalid format (missing comma between port and serial)".into());
1012 };
1013
1014 let port: u16 = parse_number(port)
1015 .map_err(|_| "could not parse port".to_owned())?
1016 .try_into()
1017 .map_err(|_| "port must be 16-bit")?;
1018 let serial: SerialConfigCli = serial.parse()?;
1019
1020 Ok(Self { port, serial })
1021 }
1022}
1023
1024#[derive(Clone, Debug, PartialEq)]
1026pub enum SerialConfigCli {
1027 None,
1028 Console,
1029 NewConsole(Option<PathBuf>, Option<String>),
1030 Stderr,
1031 Pipe(PathBuf),
1032 Tcp(SocketAddr),
1033 File(PathBuf),
1034}
1035
1036impl FromStr for SerialConfigCli {
1037 type Err = String;
1038
1039 fn from_str(s: &str) -> Result<Self, Self::Err> {
1040 let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1041
1042 let first_key = match keyvalues.first() {
1043 Some(first_pair) => first_pair.0.as_str(),
1044 None => Err("invalid serial configuration: no values supplied")?,
1045 };
1046 let first_value = keyvalues.first().unwrap().1.as_ref();
1047
1048 let ret = match first_key {
1049 "none" => SerialConfigCli::None,
1050 "console" => SerialConfigCli::Console,
1051 "stderr" => SerialConfigCli::Stderr,
1052 "file" => match first_value {
1053 Some(path) => SerialConfigCli::File(path.into()),
1054 None => Err("invalid serial configuration: file requires a value")?,
1055 },
1056 "term" => match first_value {
1057 Some(path) => {
1058 let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1060 let window_name = match window_name {
1061 Some((_, Some(name))) => Some(name.clone()),
1062 _ => None,
1063 };
1064
1065 SerialConfigCli::NewConsole(Some(path.into()), window_name)
1066 }
1067 None => SerialConfigCli::NewConsole(None, None),
1068 },
1069 "listen" => match first_value {
1070 Some(path) => {
1071 if let Some(tcp) = path.strip_prefix("tcp:") {
1072 let addr = tcp
1073 .parse()
1074 .map_err(|err| format!("invalid tcp address: {err}"))?;
1075 SerialConfigCli::Tcp(addr)
1076 } else {
1077 SerialConfigCli::Pipe(path.into())
1078 }
1079 }
1080 None => Err(
1081 "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1082 )?,
1083 },
1084 _ => {
1085 return Err(format!(
1086 "invalid serial configuration: '{}' is not a known option",
1087 first_key
1088 ));
1089 }
1090 };
1091
1092 Ok(ret)
1093 }
1094}
1095
1096impl SerialConfigCli {
1097 fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1100 let mut ret = Vec::new();
1101
1102 for item in s.split(',') {
1104 let mut eqsplit = item.split('=');
1107 let key = eqsplit.next();
1108 let value = eqsplit.next();
1109
1110 if let Some(key) = key {
1111 ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1112 } else {
1113 return Err("invalid key=value pair in serial config".into());
1115 }
1116 }
1117 Ok(ret)
1118 }
1119}
1120
1121#[derive(Clone, Debug, PartialEq)]
1122pub enum EndpointConfigCli {
1123 None,
1124 Consomme { cidr: Option<String> },
1125 Dio { id: Option<String> },
1126 Tap { name: String },
1127}
1128
1129impl FromStr for EndpointConfigCli {
1130 type Err = String;
1131
1132 fn from_str(s: &str) -> Result<Self, Self::Err> {
1133 let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1134 ["none"] => EndpointConfigCli::None,
1135 ["consomme", s @ ..] => EndpointConfigCli::Consomme {
1136 cidr: s.first().map(|&s| s.to_owned()),
1137 },
1138 ["dio", s @ ..] => EndpointConfigCli::Dio {
1139 id: s.first().map(|s| (*s).to_owned()),
1140 },
1141 ["tap", name] => EndpointConfigCli::Tap {
1142 name: (*name).to_owned(),
1143 },
1144 _ => return Err("invalid network backend".into()),
1145 };
1146
1147 Ok(ret)
1148 }
1149}
1150
1151#[derive(Clone, Debug, PartialEq)]
1152pub struct NicConfigCli {
1153 pub vtl: DeviceVtl,
1154 pub endpoint: EndpointConfigCli,
1155 pub max_queues: Option<u16>,
1156 pub underhill: bool,
1157}
1158
1159impl FromStr for NicConfigCli {
1160 type Err = String;
1161
1162 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1163 let mut vtl = DeviceVtl::Vtl0;
1164 let mut max_queues = None;
1165 let mut underhill = false;
1166 while let Some((opt, rest)) = s.split_once(':') {
1167 if let Some((opt, val)) = opt.split_once('=') {
1168 match opt {
1169 "queues" => {
1170 max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1171 }
1172 _ => break,
1173 }
1174 } else {
1175 match opt {
1176 "vtl2" => {
1177 vtl = DeviceVtl::Vtl2;
1178 }
1179 "uh" => underhill = true,
1180 _ => break,
1181 }
1182 }
1183 s = rest;
1184 }
1185
1186 if underhill && vtl != DeviceVtl::Vtl0 {
1187 return Err("`uh` is incompatible with `vtl2`".into());
1188 }
1189
1190 let endpoint = s.parse()?;
1191 Ok(NicConfigCli {
1192 vtl,
1193 endpoint,
1194 max_queues,
1195 underhill,
1196 })
1197 }
1198}
1199
1200#[derive(Debug, Error)]
1201#[error("unknown hypervisor: {0}")]
1202pub struct UnknownHypervisor(String);
1203
1204fn parse_hypervisor(s: &str) -> Result<Hypervisor, UnknownHypervisor> {
1205 match s {
1206 "kvm" => Ok(Hypervisor::Kvm),
1207 "mshv" => Ok(Hypervisor::MsHv),
1208 "whp" => Ok(Hypervisor::Whp),
1209 _ => Err(UnknownHypervisor(s.to_owned())),
1210 }
1211}
1212
1213#[derive(Debug, Error)]
1214#[error("unknown VTL2 relocation type: {0}")]
1215pub struct UnknownVtl2RelocationType(String);
1216
1217fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1218 match s {
1219 "disable" => Ok(Vtl2BaseAddressType::File),
1220 s if s.starts_with("auto=") => {
1221 let s = s.strip_prefix("auto=").unwrap_or_default();
1222 let size = if s == "filesize" {
1223 None
1224 } else {
1225 let size = parse_memory(s).map_err(|e| {
1226 UnknownVtl2RelocationType(format!(
1227 "unable to parse memory size from {} for 'auto=' type, {e}",
1228 e
1229 ))
1230 })?;
1231 Some(size)
1232 };
1233 Ok(Vtl2BaseAddressType::MemoryLayout { size })
1234 }
1235 s if s.starts_with("absolute=") => {
1236 let s = s.strip_prefix("absolute=");
1237 let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
1238 UnknownVtl2RelocationType(format!(
1239 "unable to parse number from {} for 'absolute=' type",
1240 e
1241 ))
1242 })?;
1243 Ok(Vtl2BaseAddressType::Absolute(addr))
1244 }
1245 s if s.starts_with("vtl2=") => {
1246 let s = s.strip_prefix("vtl2=").unwrap_or_default();
1247 let size = if s == "filesize" {
1248 None
1249 } else {
1250 let size = parse_memory(s).map_err(|e| {
1251 UnknownVtl2RelocationType(format!(
1252 "unable to parse memory size from {} for 'vtl2=' type, {e}",
1253 e
1254 ))
1255 })?;
1256 Some(size)
1257 };
1258 Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
1259 }
1260 _ => Err(UnknownVtl2RelocationType(s.to_owned())),
1261 }
1262}
1263
1264#[derive(Debug, Copy, Clone, PartialEq)]
1265pub enum SmtConfigCli {
1266 Auto,
1267 Force,
1268 Off,
1269}
1270
1271#[derive(Debug, Error)]
1272#[error("expected auto, force, or off")]
1273pub struct BadSmtConfig;
1274
1275impl FromStr for SmtConfigCli {
1276 type Err = BadSmtConfig;
1277
1278 fn from_str(s: &str) -> Result<Self, Self::Err> {
1279 let r = match s {
1280 "auto" => Self::Auto,
1281 "force" => Self::Force,
1282 "off" => Self::Off,
1283 _ => return Err(BadSmtConfig),
1284 };
1285 Ok(r)
1286 }
1287}
1288
1289#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
1290fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
1291 let r = match s {
1292 "auto" => X2ApicConfig::Auto,
1293 "supported" => X2ApicConfig::Supported,
1294 "off" => X2ApicConfig::Unsupported,
1295 "on" => X2ApicConfig::Enabled,
1296 _ => return Err("expected auto, supported, off, or on"),
1297 };
1298 Ok(r)
1299}
1300
1301#[derive(Debug, Copy, Clone, ValueEnum)]
1302pub enum Vtl0LateMapPolicyCli {
1303 Off,
1304 Log,
1305 Halt,
1306 Exception,
1307}
1308
1309#[derive(Debug, Copy, Clone, ValueEnum)]
1310pub enum IsolationCli {
1311 Vbs,
1312}
1313
1314#[derive(Debug, Copy, Clone, PartialEq)]
1315pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
1316
1317impl FromStr for PcatBootOrderCli {
1318 type Err = &'static str;
1319
1320 fn from_str(s: &str) -> Result<Self, Self::Err> {
1321 let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
1322 let mut order = Vec::new();
1323
1324 for item in s.split(',') {
1325 let device = match item {
1326 "optical" => PcatBootDevice::Optical,
1327 "hdd" => PcatBootDevice::HardDrive,
1328 "net" => PcatBootDevice::Network,
1329 "floppy" => PcatBootDevice::Floppy,
1330 _ => return Err("unknown boot device type"),
1331 };
1332
1333 let default_pos = default_order
1334 .iter()
1335 .position(|x| x == &Some(device))
1336 .ok_or("cannot pass duplicate boot devices")?;
1337
1338 order.push(default_order[default_pos].take().unwrap());
1339 }
1340
1341 order.extend(default_order.into_iter().flatten());
1342 assert_eq!(order.len(), 4);
1343
1344 Ok(Self(order.try_into().unwrap()))
1345 }
1346}
1347
1348#[derive(Copy, Clone, Debug, ValueEnum)]
1349pub enum UefiConsoleModeCli {
1350 Default,
1351 Com1,
1352 Com2,
1353 None,
1354}
1355
1356fn default_value_from_arch_env(name: &str) -> OsString {
1364 let prefix = if cfg!(guest_arch = "x86_64") {
1365 "X86_64"
1366 } else if cfg!(guest_arch = "aarch64") {
1367 "AARCH64"
1368 } else {
1369 return Default::default();
1370 };
1371 let prefixed = format!("{}_{}", prefix, name);
1372 std::env::var_os(name)
1373 .or_else(|| std::env::var_os(prefixed))
1374 .unwrap_or_default()
1375}
1376
1377#[derive(Clone)]
1379pub struct OptionalPathBuf(pub Option<PathBuf>);
1380
1381impl From<&std::ffi::OsStr> for OptionalPathBuf {
1382 fn from(s: &std::ffi::OsStr) -> Self {
1383 OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
1384 }
1385}
1386
1387#[cfg(test)]
1388#[expect(unsafe_code)]
1390mod tests {
1391 use super::*;
1392
1393 fn with_env_var<F, R>(name: &str, value: &str, f: F) -> R
1394 where
1395 F: FnOnce() -> R,
1396 {
1397 unsafe {
1400 std::env::set_var(name, value);
1401 }
1402 let result = f();
1403 unsafe {
1406 std::env::remove_var(name);
1407 }
1408 result
1409 }
1410
1411 #[test]
1412 fn test_parse_file_disk_with_create() {
1413 let s = "file:test.vhd;create=1G";
1414 let disk = DiskCliKind::from_str(s).unwrap();
1415
1416 match disk {
1417 DiskCliKind::File {
1418 path,
1419 create_with_len,
1420 } => {
1421 assert_eq!(path, PathBuf::from("test.vhd"));
1422 assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); }
1424 _ => panic!("Expected File variant"),
1425 }
1426 }
1427
1428 #[test]
1429 fn test_parse_direct_file_with_create() {
1430 let s = "test.vhd;create=1G";
1431 let disk = DiskCliKind::from_str(s).unwrap();
1432
1433 match disk {
1434 DiskCliKind::File {
1435 path,
1436 create_with_len,
1437 } => {
1438 assert_eq!(path, PathBuf::from("test.vhd"));
1439 assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); }
1441 _ => panic!("Expected File variant"),
1442 }
1443 }
1444
1445 #[test]
1446 fn test_parse_memory_disk() {
1447 let s = "mem:1G";
1448 let disk = DiskCliKind::from_str(s).unwrap();
1449 match disk {
1450 DiskCliKind::Memory(size) => {
1451 assert_eq!(size, 1024 * 1024 * 1024); }
1453 _ => panic!("Expected Memory variant"),
1454 }
1455 }
1456
1457 #[test]
1458 fn test_parse_memory_diff_disk() {
1459 let s = "memdiff:file:base.img";
1460 let disk = DiskCliKind::from_str(s).unwrap();
1461 match disk {
1462 DiskCliKind::MemoryDiff(inner) => match *inner {
1463 DiskCliKind::File {
1464 path,
1465 create_with_len,
1466 } => {
1467 assert_eq!(path, PathBuf::from("base.img"));
1468 assert_eq!(create_with_len, None);
1469 }
1470 _ => panic!("Expected File variant inside MemoryDiff"),
1471 },
1472 _ => panic!("Expected MemoryDiff variant"),
1473 }
1474 }
1475
1476 #[test]
1477 fn test_parse_sqlite_disk() {
1478 let s = "sql:db.sqlite;create=2G";
1479 let disk = DiskCliKind::from_str(s).unwrap();
1480 match disk {
1481 DiskCliKind::Sqlite {
1482 path,
1483 create_with_len,
1484 } => {
1485 assert_eq!(path, PathBuf::from("db.sqlite"));
1486 assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
1487 }
1488 _ => panic!("Expected Sqlite variant"),
1489 }
1490
1491 let s = "sql:db.sqlite";
1493 let disk = DiskCliKind::from_str(s).unwrap();
1494 match disk {
1495 DiskCliKind::Sqlite {
1496 path,
1497 create_with_len,
1498 } => {
1499 assert_eq!(path, PathBuf::from("db.sqlite"));
1500 assert_eq!(create_with_len, None);
1501 }
1502 _ => panic!("Expected Sqlite variant"),
1503 }
1504 }
1505
1506 #[test]
1507 fn test_parse_sqlite_diff_disk() {
1508 let s = "sqldiff:diff.sqlite;create:file:base.img";
1510 let disk = DiskCliKind::from_str(s).unwrap();
1511 match disk {
1512 DiskCliKind::SqliteDiff { path, create, disk } => {
1513 assert_eq!(path, PathBuf::from("diff.sqlite"));
1514 assert!(create);
1515 match *disk {
1516 DiskCliKind::File {
1517 path,
1518 create_with_len,
1519 } => {
1520 assert_eq!(path, PathBuf::from("base.img"));
1521 assert_eq!(create_with_len, None);
1522 }
1523 _ => panic!("Expected File variant inside SqliteDiff"),
1524 }
1525 }
1526 _ => panic!("Expected SqliteDiff variant"),
1527 }
1528
1529 let s = "sqldiff:diff.sqlite:file:base.img";
1531 let disk = DiskCliKind::from_str(s).unwrap();
1532 match disk {
1533 DiskCliKind::SqliteDiff { path, create, disk } => {
1534 assert_eq!(path, PathBuf::from("diff.sqlite"));
1535 assert!(!create);
1536 match *disk {
1537 DiskCliKind::File {
1538 path,
1539 create_with_len,
1540 } => {
1541 assert_eq!(path, PathBuf::from("base.img"));
1542 assert_eq!(create_with_len, None);
1543 }
1544 _ => panic!("Expected File variant inside SqliteDiff"),
1545 }
1546 }
1547 _ => panic!("Expected SqliteDiff variant"),
1548 }
1549 }
1550
1551 #[test]
1552 fn test_parse_autocache_sqlite_disk() {
1553 let disk = with_env_var("OPENVMM_AUTO_CACHE_PATH", "/tmp/cache", || {
1555 DiskCliKind::from_str("autocache::file:disk.vhd").unwrap()
1556 });
1557 assert!(matches!(
1558 disk,
1559 DiskCliKind::AutoCacheSqlite {
1560 cache_path,
1561 key,
1562 disk: _disk,
1563 } if cache_path == "/tmp/cache" && key.is_none()
1564 ));
1565
1566 assert!(DiskCliKind::from_str("autocache::file:disk.vhd").is_err());
1568 }
1569
1570 #[test]
1571 fn test_parse_disk_errors() {
1572 assert!(DiskCliKind::from_str("invalid:").is_err());
1573 assert!(DiskCliKind::from_str("memory:extra").is_err());
1574
1575 assert!(DiskCliKind::from_str("sqlite:").is_err());
1577 }
1578
1579 #[test]
1580 fn test_parse_errors() {
1581 assert!(DiskCliKind::from_str("mem:invalid").is_err());
1583
1584 assert!(DiskCliKind::from_str("sqldiff:path").is_err());
1586
1587 #[allow(deprecated_safe_2024)]
1589 std::env::remove_var("OPENVMM_AUTO_CACHE_PATH");
1590 assert!(DiskCliKind::from_str("autocache:key:file:disk.vhd").is_err());
1591
1592 assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
1594
1595 assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
1597
1598 assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
1600
1601 assert!(DiskCliKind::from_str("invalid:path").is_err());
1603
1604 assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
1606 }
1607
1608 #[test]
1609 fn test_fs_args_from_str() {
1610 let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
1611 assert_eq!(args.tag, "tag1");
1612 assert_eq!(args.path, "/path/to/fs");
1613
1614 assert!(FsArgs::from_str("tag1").is_err());
1616 assert!(FsArgs::from_str("tag1,/path,extra").is_err());
1617 }
1618
1619 #[test]
1620 fn test_fs_args_with_options_from_str() {
1621 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
1622 assert_eq!(args.tag, "tag1");
1623 assert_eq!(args.path, "/path/to/fs");
1624 assert_eq!(args.options, "opt1;opt2");
1625
1626 let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
1628 assert_eq!(args.tag, "tag1");
1629 assert_eq!(args.path, "/path/to/fs");
1630 assert_eq!(args.options, "");
1631
1632 assert!(FsArgsWithOptions::from_str("tag1").is_err());
1634 }
1635
1636 #[test]
1637 fn test_serial_config_from_str() {
1638 assert_eq!(
1639 SerialConfigCli::from_str("none").unwrap(),
1640 SerialConfigCli::None
1641 );
1642 assert_eq!(
1643 SerialConfigCli::from_str("console").unwrap(),
1644 SerialConfigCli::Console
1645 );
1646 assert_eq!(
1647 SerialConfigCli::from_str("stderr").unwrap(),
1648 SerialConfigCli::Stderr
1649 );
1650
1651 let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
1653 if let SerialConfigCli::File(path) = file_config {
1654 assert_eq!(path.to_str().unwrap(), "/path/to/file");
1655 } else {
1656 panic!("Expected File variant");
1657 }
1658
1659 match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
1661 SerialConfigCli::NewConsole(Some(path), Some(name)) => {
1662 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
1663 assert_eq!(name, "MyTerm");
1664 }
1665 _ => panic!("Expected NewConsole variant with name"),
1666 }
1667
1668 match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
1670 SerialConfigCli::NewConsole(Some(path), None) => {
1671 assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
1672 }
1673 _ => panic!("Expected NewConsole variant without name"),
1674 }
1675
1676 match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
1678 SerialConfigCli::Tcp(addr) => {
1679 assert_eq!(addr.to_string(), "127.0.0.1:1234");
1680 }
1681 _ => panic!("Expected Tcp variant"),
1682 }
1683
1684 match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
1686 SerialConfigCli::Pipe(path) => {
1687 assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
1688 }
1689 _ => panic!("Expected Pipe variant"),
1690 }
1691
1692 assert!(SerialConfigCli::from_str("").is_err());
1694 assert!(SerialConfigCli::from_str("unknown").is_err());
1695 assert!(SerialConfigCli::from_str("file").is_err());
1696 assert!(SerialConfigCli::from_str("listen").is_err());
1697 }
1698
1699 #[test]
1700 fn test_endpoint_config_from_str() {
1701 assert!(matches!(
1703 EndpointConfigCli::from_str("none").unwrap(),
1704 EndpointConfigCli::None
1705 ));
1706
1707 match EndpointConfigCli::from_str("consomme").unwrap() {
1709 EndpointConfigCli::Consomme { cidr: None } => (),
1710 _ => panic!("Expected Consomme variant without cidr"),
1711 }
1712
1713 match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
1715 EndpointConfigCli::Consomme { cidr: Some(cidr) } => {
1716 assert_eq!(cidr, "192.168.0.0/24");
1717 }
1718 _ => panic!("Expected Consomme variant with cidr"),
1719 }
1720
1721 match EndpointConfigCli::from_str("dio").unwrap() {
1723 EndpointConfigCli::Dio { id: None } => (),
1724 _ => panic!("Expected Dio variant without id"),
1725 }
1726
1727 match EndpointConfigCli::from_str("dio:test_id").unwrap() {
1729 EndpointConfigCli::Dio { id: Some(id) } => {
1730 assert_eq!(id, "test_id");
1731 }
1732 _ => panic!("Expected Dio variant with id"),
1733 }
1734
1735 match EndpointConfigCli::from_str("tap:tap0").unwrap() {
1737 EndpointConfigCli::Tap { name } => {
1738 assert_eq!(name, "tap0");
1739 }
1740 _ => panic!("Expected Tap variant"),
1741 }
1742
1743 assert!(EndpointConfigCli::from_str("invalid").is_err());
1745 }
1746
1747 #[test]
1748 fn test_nic_config_from_str() {
1749 use hvlite_defs::config::DeviceVtl;
1750
1751 let config = NicConfigCli::from_str("none").unwrap();
1753 assert_eq!(config.vtl, DeviceVtl::Vtl0);
1754 assert!(config.max_queues.is_none());
1755 assert!(!config.underhill);
1756 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1757
1758 let config = NicConfigCli::from_str("vtl2:none").unwrap();
1760 assert_eq!(config.vtl, DeviceVtl::Vtl2);
1761 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1762
1763 let config = NicConfigCli::from_str("queues=4:none").unwrap();
1765 assert_eq!(config.max_queues, Some(4));
1766 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1767
1768 let config = NicConfigCli::from_str("uh:none").unwrap();
1770 assert!(config.underhill);
1771 assert!(matches!(config.endpoint, EndpointConfigCli::None));
1772
1773 assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
1775 assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); }
1777
1778 #[test]
1779 fn test_smt_config_from_str() {
1780 assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
1781 assert_eq!(
1782 SmtConfigCli::from_str("force").unwrap(),
1783 SmtConfigCli::Force
1784 );
1785 assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
1786
1787 assert!(SmtConfigCli::from_str("invalid").is_err());
1789 assert!(SmtConfigCli::from_str("").is_err());
1790 }
1791
1792 #[test]
1793 fn test_pcat_boot_order_from_str() {
1794 let order = PcatBootOrderCli::from_str("optical").unwrap();
1796 assert_eq!(order.0[0], PcatBootDevice::Optical);
1797
1798 let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
1800 assert_eq!(order.0[0], PcatBootDevice::HardDrive);
1801 assert_eq!(order.0[1], PcatBootDevice::Network);
1802
1803 assert!(PcatBootOrderCli::from_str("invalid").is_err());
1805 assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); }
1807
1808 #[test]
1809 fn test_floppy_disk_from_str() {
1810 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
1812 assert!(!disk.read_only);
1813 match disk.kind {
1814 DiskCliKind::File {
1815 path,
1816 create_with_len,
1817 } => {
1818 assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
1819 assert_eq!(create_with_len, None);
1820 }
1821 _ => panic!("Expected File variant"),
1822 }
1823
1824 let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
1826 assert!(disk.read_only);
1827
1828 assert!(FloppyDiskCli::from_str("").is_err());
1830 assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
1831 }
1832}