openvmm_entry/
cli_args.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! CLI argument parsing.
5//!
6//! Code in this module must not instantiate any complex VM objects!
7//!
8//! In other words, this module is only responsible for marshalling raw CLI
9//! strings into typed Rust structs/enums, and should consist of entirely _pure
10//! functions_.
11//!
12//! e.g: instead of opening a `File` directly, parse the specified file path
13//! into a `PathBuf`, and allow later parts of the init flow to handle opening
14//! the file.
15
16// NOTE: This module itself is not pub, but the Options struct below is
17//       re-exported as pub in main to make this lint fire. It won't fire on
18//       anything else on this file though.
19#![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/// OpenVMM virtual machine monitor.
37///
38/// This is not yet a stable interface and may change radically between
39/// versions.
40#[derive(Parser)]
41pub struct Options {
42    /// processor count
43    #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
44    pub processors: u32,
45
46    /// guest RAM size
47    #[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    /// use shared memory segment
57    #[clap(short = 'M', long)]
58    pub shared_memory: bool,
59
60    /// prefetch guest RAM
61    #[clap(long)]
62    pub prefetch: bool,
63
64    /// start in paused state
65    #[clap(short = 'P', long)]
66    pub paused: bool,
67
68    /// kernel image (when using linux direct boot)
69    #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
70    pub kernel: OptionalPathBuf,
71
72    /// initrd image (when using linux direct boot)
73    #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
74    pub initrd: OptionalPathBuf,
75
76    /// extra kernel command line args
77    #[clap(short = 'c', long, value_name = "STRING")]
78    pub cmdline: Vec<String>,
79
80    /// enable HV#1 capabilities
81    #[clap(long)]
82    pub hv: bool,
83
84    /// enable vtl2 - only supported in WHP and simulated without hypervisor support currently
85    ///
86    /// Currently implies --get.
87    #[clap(long, requires("hv"))]
88    pub vtl2: bool,
89
90    /// Add GET and related devices for using the OpenHCL paravisor to the
91    /// highest enabled VTL.
92    #[clap(long, requires("hv"))]
93    pub get: bool,
94
95    /// Disable GET and related devices for using the OpenHCL paravisor, even
96    /// when --vtl2 is passed.
97    #[clap(long, conflicts_with("get"))]
98    pub no_get: bool,
99
100    /// The disk to use for the GET VMGS.
101    ///
102    /// If this is not provided, then a 4MB RAM disk will be used.
103    #[clap(long)]
104    pub get_vmgs: Option<DiskCliKind>,
105
106    /// disable the VTL0 alias map presented to VTL2 by default
107    #[clap(long, requires("vtl2"))]
108    pub no_alias_map: bool,
109
110    /// enable isolation emulation
111    #[clap(long, requires("vtl2"))]
112    pub isolation: Option<IsolationCli>,
113
114    /// the hybrid vsock listener path
115    #[clap(long, value_name = "PATH")]
116    pub vsock_path: Option<String>,
117
118    /// the VTL2 hybrid vsock listener path
119    #[clap(long, value_name = "PATH", requires("vtl2"))]
120    pub vtl2_vsock_path: Option<String>,
121
122    /// the late map vtl0 ram access policy when vtl2 is enabled
123    #[clap(long, requires("vtl2"), default_value = "halt")]
124    pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
125
126    /// disable in-hypervisor enlightenment implementation (where possible)
127    #[clap(long)]
128    pub no_enlightenments: bool,
129
130    /// disable the in-hypervisor APIC and use the user-mode one (where possible)
131    #[clap(long)]
132    pub user_mode_apic: bool,
133
134    /// attach a disk (can be passed multiple times)
135    #[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    /// attach a disk via an NVMe controller
158    #[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    /// number of sub-channels for the SCSI controller
179    #[clap(long, value_name = "COUNT", default_value = "0")]
180    pub scsi_sub_channels: u16,
181
182    /// expose a virtual NIC
183    #[clap(long)]
184    pub nic: bool,
185
186    /// expose a virtual NIC with the given backend (consomme | dio | tap | none)
187    ///
188    /// Prefix with `uh:` to add this NIC via Mana emulation through Underhill,
189    /// or `vtl2:` to assign this NIC to VTL2.
190    #[clap(long)]
191    pub net: Vec<NicConfigCli>,
192
193    /// expose a virtual NIC using the Windows kernel-mode vmswitch.
194    ///
195    /// Specify the switch ID or "default" for the default switch.
196    #[clap(long, value_name = "SWITCH_ID")]
197    pub kernel_vmnic: Vec<String>,
198
199    /// expose a graphics device
200    #[clap(long)]
201    pub gfx: bool,
202
203    /// support a graphics device in vtl2
204    #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
205    pub vtl2_gfx: bool,
206
207    /// listen for vnc connections. implied by gfx.
208    #[clap(long)]
209    pub vnc: bool,
210
211    /// VNC port number
212    #[clap(long, value_name = "PORT", default_value = "5900")]
213    pub vnc_port: u16,
214
215    /// set the APIC ID offset, for testing APIC IDs that don't match VP index
216    #[cfg(guest_arch = "x86_64")]
217    #[clap(long, default_value_t)]
218    pub apic_id_offset: u32,
219
220    /// the maximum number of VPs per socket
221    #[clap(long)]
222    pub vps_per_socket: Option<u32>,
223
224    /// enable or disable SMT (hyperthreading) (auto | force | off)
225    #[clap(long, default_value = "auto")]
226    pub smt: SmtConfigCli,
227
228    /// configure x2apic (auto | supported | off | on)
229    #[cfg(guest_arch = "x86_64")]
230    #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
231    pub x2apic: X2ApicConfig,
232
233    /// use virtio console
234    #[clap(long)]
235    pub virtio_console: bool,
236
237    /// use virtio console enumerated via VPCI
238    #[clap(long, conflicts_with("virtio_console"))]
239    pub virtio_console_pci: bool,
240
241    /// COM1 binding (console | stderr | listen=\<path\> | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
242    #[clap(long, value_name = "SERIAL")]
243    pub com1: Option<SerialConfigCli>,
244
245    /// COM2 binding (console | stderr | listen=\<path\> | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
246    #[clap(long, value_name = "SERIAL")]
247    pub com2: Option<SerialConfigCli>,
248
249    /// COM3 binding (console | stderr | listen=\<path\> | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
250    #[clap(long, value_name = "SERIAL")]
251    pub com3: Option<SerialConfigCli>,
252
253    /// COM4 binding (console | stderr | listen=\<path\> | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
254    #[clap(long, value_name = "SERIAL")]
255    pub com4: Option<SerialConfigCli>,
256
257    /// virtio serial binding (console | stderr | listen=\<path\> | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
258    #[clap(long, value_name = "SERIAL")]
259    pub virtio_serial: Option<SerialConfigCli>,
260
261    /// vmbus com1 serial binding (console | stderr | listen=\<path\> | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
262    #[structopt(long, value_name = "SERIAL")]
263    pub vmbus_com1_serial: Option<SerialConfigCli>,
264
265    /// vmbus com2 serial binding (console | stderr | listen=\<path\> | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
266    #[structopt(long, value_name = "SERIAL")]
267    pub vmbus_com2_serial: Option<SerialConfigCli>,
268
269    /// debugcon binding (port:serial, where port is a u16, and serial is (console | stderr | listen=\<path\> | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none))
270    #[clap(long, value_name = "SERIAL")]
271    pub debugcon: Option<DebugconSerialConfigCli>,
272
273    /// boot UEFI firmware
274    #[clap(long, short = 'e')]
275    pub uefi: bool,
276
277    /// UEFI firmware file
278    #[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    /// enable UEFI debugging on COM1
282    #[clap(long, requires("uefi"))]
283    pub uefi_debug: bool,
284
285    /// enable memory protections in UEFI
286    #[clap(long, requires("uefi"))]
287    pub uefi_enable_memory_protections: bool,
288
289    /// set PCAT boot order as comma-separated string of boot device types
290    /// (e.g: floppy,hdd,optical,net).
291    ///
292    /// If less than 4 entries are added, entries are added according to their
293    /// default boot order (optical,hdd,net,floppy)
294    ///
295    /// e.g: passing "floppy,optical" will result in a boot order equivalent to
296    /// "floppy,optical,hdd,net".
297    ///
298    /// Passing duplicate types is an error.
299    #[clap(long, requires("pcat"))]
300    pub pcat_boot_order: Option<PcatBootOrderCli>,
301
302    /// Boot with PCAT BIOS firmware and piix4 devices
303    #[clap(long, conflicts_with("uefi"))]
304    pub pcat: bool,
305
306    /// PCAT firmware file
307    #[clap(long, requires("pcat"), value_name = "FILE")]
308    pub pcat_firmware: Option<PathBuf>,
309
310    /// boot IGVM file
311    #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
312    pub igvm: Option<PathBuf>,
313
314    /// specify igvm vtl2 relocation type
315    /// (absolute=\<addr\>, disable, auto=\<filesize,or memory size\>, vtl2=\<filesize,or memory size\>,)
316    #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
317    pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
318
319    /// add a virtio_9p device (e.g. myfs,C:\)
320    #[clap(long, value_name = "tag,root_path")]
321    pub virtio_9p: Vec<FsArgs>,
322
323    /// output debug info from the 9p server
324    #[clap(long)]
325    pub virtio_9p_debug: bool,
326
327    /// add a virtio_fs device (e.g. myfs,C:\,uid=1000,gid=2000)
328    #[clap(long, value_name = "tag,root_path,[options]")]
329    pub virtio_fs: Vec<FsArgsWithOptions>,
330
331    /// add a virtio_fs device for sharing memory (e.g. myfs,\SectionDirectoryPath)
332    #[clap(long, value_name = "tag,root_path")]
333    pub virtio_fs_shmem: Vec<FsArgs>,
334
335    /// add a virtio_fs device under either the PCI or MMIO bus, or whatever the hypervisor supports (pci | mmio | auto)
336    #[clap(long, value_name = "BUS", default_value = "auto")]
337    pub virtio_fs_bus: VirtioBusCli,
338
339    /// virtio PMEM device
340    #[clap(long, value_name = "PATH")]
341    pub virtio_pmem: Option<String>,
342
343    /// expose a virtio network with the given backend (dio | vmnic | tap |
344    /// none)
345    ///
346    /// Prefix with `uh:` to add this NIC via Mana emulation through Underhill,
347    /// or `vtl2:` to assign this NIC to VTL2.
348    #[clap(long)]
349    pub virtio_net: Vec<NicConfigCli>,
350
351    /// send log output from the worker process to a file instead of stderr. the file will be overwritten.
352    #[clap(long, value_name = "PATH")]
353    pub log_file: Option<PathBuf>,
354
355    /// run as a ttrpc server on the specified Unix socket
356    #[clap(long, value_name = "SOCKETPATH")]
357    pub ttrpc: Option<PathBuf>,
358
359    /// run as a grpc server on the specified Unix socket
360    #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
361    pub grpc: Option<PathBuf>,
362
363    /// do not launch child processes
364    #[clap(long)]
365    pub single_process: bool,
366
367    /// device to assign (can be passed multiple times)
368    #[cfg(windows)]
369    #[clap(long, value_name = "PATH")]
370    pub device: Vec<String>,
371
372    /// instead of showing the frontpage the VM will shutdown instead
373    #[clap(long, requires("uefi"))]
374    pub disable_frontpage: bool,
375
376    /// add a vtpm device
377    #[clap(long)]
378    pub tpm: bool,
379
380    /// the mesh worker host name.
381    ///
382    /// Used internally for debugging and diagnostics.
383    #[clap(long, default_value = "control", hide(true))]
384    #[expect(clippy::option_option)]
385    pub internal_worker: Option<Option<String>>,
386
387    /// redirect the VTL 0 vmbus control plane to a proxy in VTL 2.
388    #[clap(long, requires("vtl2"))]
389    pub vmbus_redirect: bool,
390
391    /// limit the maximum protocol version allowed by vmbus; used for testing purposes
392    #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
393    pub vmbus_max_version: Option<u32>,
394
395    /// path to vmgs file. if no file is provided, fallback to in-memory vmgs implementation
396    #[clap(long, value_name = "PATH")]
397    pub vmgs_file: Option<PathBuf>,
398
399    /// VGA firmware file
400    #[clap(long, requires("pcat"), value_name = "FILE")]
401    pub vga_firmware: Option<PathBuf>,
402
403    /// enable secure boot
404    #[clap(long)]
405    pub secure_boot: bool,
406
407    /// use secure boot template
408    #[clap(long)]
409    pub secure_boot_template: Option<SecureBootTemplateCli>,
410
411    /// custom uefi nvram json file
412    #[clap(long, value_name = "PATH")]
413    pub custom_uefi_json: Option<PathBuf>,
414
415    /// the path to a named pipe (Windows) or Unix socket (Linux) to relay to the connected
416    /// tty.
417    ///
418    /// This is a hidden argument used internally.
419    #[clap(long, hide(true))]
420    pub relay_console_path: Option<PathBuf>,
421
422    /// the title of the console window spawned from the relay console.
423    ///
424    /// This is a hidden argument used internally.
425    #[clap(long, hide(true))]
426    pub relay_console_title: Option<String>,
427
428    /// enable in-hypervisor gdb debugger
429    #[clap(long, value_name = "PORT")]
430    pub gdb: Option<u16>,
431
432    /// enable emulated MANA devices with the given network backend (see --net)
433    #[clap(long)]
434    pub mana: Vec<NicConfigCli>,
435
436    /// use a specific hypervisor interface
437    #[clap(long, value_parser = parse_hypervisor)]
438    pub hypervisor: Option<Hypervisor>,
439
440    /// (dev utility) boot linux using a custom (raw) DSDT table.
441    ///
442    /// This is a _very_ niche utility, and it's unlikely you'll need to use it.
443    ///
444    /// e.g: this flag helped bring up certain Hyper-V Generation 1 legacy
445    /// devices without needing to port the associated ACPI code into HvLite's
446    /// DSDT builder.
447    #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
448    pub custom_dsdt: Option<PathBuf>,
449
450    /// attach an ide drive (can be passed multiple times)
451    ///
452    /// Each ide controller has two channels. Each channel can have up to two
453    /// attachments.
454    ///
455    /// If the `s` flag is not passed then the drive will we be attached to the
456    /// primary ide channel if space is available. If two attachments have already
457    /// been added to the primary channel then the drive will be attached to the
458    /// secondary channel.
459    #[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    /// attach a floppy drive (should be able to be passed multiple times). VM must be generation 1 (no UEFI)
481    ///
482    #[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    /// enable guest watchdog device
502    #[clap(long)]
503    pub guest_watchdog: bool,
504
505    /// enable OpenHCL's guest crash dump device, targeting the specified path
506    #[clap(long)]
507    pub openhcl_dump_path: Option<PathBuf>,
508
509    /// halt the VM when the guest requests a reset, instead of resetting it
510    #[clap(long)]
511    pub halt_on_reset: bool,
512
513    /// write saved state .proto files to the specified path
514    #[clap(long)]
515    pub write_saved_state_proto: Option<PathBuf>,
516
517    /// specify the IMC hive file for booting Windows
518    #[clap(long)]
519    pub imc: Option<PathBuf>,
520
521    /// Expose MCR device
522    #[clap(long)]
523    pub mcr: bool, // TODO MCR: support closed source CLI flags
524
525    /// expose a battery device
526    #[clap(long)]
527    pub battery: bool,
528
529    /// set the uefi console mode
530    #[clap(long)]
531    pub uefi_console_mode: Option<UefiConsoleModeCli>,
532
533    /// Perform a default boot even if boot entries exist and fail
534    #[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    /// The file system tag.
562    pub tag: String,
563    /// The root path.
564    pub path: String,
565    /// The extra options, joined with ';'.
566    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
625/// Parse a number from a string that could be prefixed with 0x to indicate hex.
626fn 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    // mem:<len>
636    Memory(u64),
637    // memdiff:<kind>
638    MemoryDiff(Box<DiskCliKind>),
639    // sql:<path>[;create=<len>]
640    Sqlite {
641        path: PathBuf,
642        create_with_len: Option<u64>,
643    },
644    // sqldiff:<path>[;create]:<kind>
645    SqliteDiff {
646        path: PathBuf,
647        create: bool,
648        disk: Box<DiskCliKind>,
649    },
650    // autocache:[key]:<kind>
651    AutoCacheSqlite {
652        cache_path: String,
653        key: Option<String>,
654        disk: Box<DiskCliKind>,
655    },
656    // prwrap:<kind>
657    PersistentReservationsWrapper(Box<DiskCliKind>),
658    // file:<path>
659    File(PathBuf),
660    // blob:<type>:<url>
661    Blob {
662        kind: BlobKind,
663        url: String,
664    },
665    // crypt:<cipher>:<key_file>:<kind>
666    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            // convenience support for passing bare paths as file disks
691            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                    // here's a fun edge case: what if the user passes `--disk d:\path\to\disk.img`?
772                    //
773                    // in this case, we actually want to treat that leading `d:` as part of the
774                    // path, rather than as a disk with `kind == 'd'`
775                    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// <kind>[,ro]
789#[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// <kind>[,ro,s]
848#[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// <kind>[,ro]
896#[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/// (console | stderr | listen=\<path\> | listen=tcp:\<ip\>:\<port\> | none)
948#[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                    // If user supplies a name key, use it to title the window
977                    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    /// Parse a comma separated list of key=value options into a vector of
1016    /// key/value pairs.
1017    fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1018        let mut ret = Vec::new();
1019
1020        // For each comma separated item in the supplied list
1021        for item in s.split(',') {
1022            // Split on the = for key and value
1023            // If no = is found, treat key as key and value as None
1024            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                // An empty key is invalid
1032                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
1274/// Read a environment variable that may / may-not have a target-specific
1275/// prefix. e.g: `default_value_from_arch_env("FOO")` would first try and read
1276/// from `FOO`, and if that's not found, it will try `X86_64_FOO`.
1277///
1278/// Must return an `OsString`, in order to be compatible with `clap`'s
1279/// default_value code. As such - to encode the absence of the env-var, an empty
1280/// OsString is returned.
1281fn 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/// Workaround to use `Option<PathBuf>` alongside [`default_value_from_arch_env`]
1296#[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}