Skip to main content

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 cxl_spec::spec::CfmwsWindowRestrictions;
25use guid::Guid;
26use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
27use openvmm_defs::config::DeviceVtl;
28use openvmm_defs::config::PcatBootDevice;
29use openvmm_defs::config::Vtl2BaseAddressType;
30use openvmm_defs::config::X2ApicConfig;
31use std::ffi::OsString;
32use std::net::SocketAddr;
33use std::path::PathBuf;
34use std::str::FromStr;
35use thiserror::Error;
36
37/// Parse CLI options, using a thread with a larger stack on Windows to avoid
38/// stack overflow in debug builds due to clap's deep stack usage.
39/// See <https://github.com/clap-rs/clap/issues/5134>.
40pub(crate) fn parse_options() -> Options {
41    // In non-optimized builds, clap uses an embarrassing amount of stack space
42    // to construct the `Command` instance for `Options`, more than the Windows
43    // default of 1MB. This has been known since 2023:
44    // <https://github.com/clap-rs/clap/issues/5134>, but no one has stepped up
45    // to fix it.
46    //
47    // Work around this by running the code on a thread with lots of stack
48    // space. This is easier and more reliable than configuring the PE binary to
49    // have a larger stack.
50    fn on_big_stack<R: Send>(f: impl Send + FnOnce() -> R) -> R {
51        if cfg!(windows) {
52            std::thread::scope(|s| {
53                std::thread::Builder::new()
54                    .stack_size(0x400000)
55                    .spawn_scoped(s, f)
56                    .unwrap()
57                    .join()
58                    .unwrap()
59            })
60        } else {
61            f()
62        }
63    }
64
65    on_big_stack(Options::parse)
66}
67
68const DEFAULT_MEMORY_SIZE: u64 = 1024 * 1024 * 1024;
69
70/// Guest memory configuration parsed from `--memory` (and, flattened, from
71/// `--numa`).
72///
73/// Fields are the raw parsed options; callers apply the defaults (`size`
74/// defaults to [`DEFAULT_MEMORY_SIZE`]; `transparent_hugepages` defaults to
75/// `!hugepages`). Cross-field validation lives in [`MemoryCli::validate`].
76#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
77pub struct MemoryCli {
78    /// Guest RAM size. Defaults to [`DEFAULT_MEMORY_SIZE`] when unset.
79    pub size: Option<vmm_cli::MemorySize>,
80    /// Whether shared file-backed memory was explicitly requested (tri-state:
81    /// unset / `on` / `off`).
82    #[kv(flag)]
83    pub shared: Option<bool>,
84    /// Whether to prefetch guest RAM.
85    #[kv(flag)]
86    pub prefetch: bool,
87    /// Whether to use transparent huge pages. When unset, defaults to enabled
88    /// unless `hugepages` is set.
89    #[kv(flag, key = "thp")]
90    pub transparent_hugepages: Option<bool>,
91    /// Whether to use explicit hugetlb memfd backing for guest RAM.
92    #[kv(flag)]
93    pub hugepages: bool,
94    /// Explicit hugetlb page size.
95    pub hugepage_size: Option<vmm_cli::MemorySize>,
96    /// File used to back guest RAM.
97    pub file: Option<PathBuf>,
98}
99
100impl MemoryCli {
101    /// Validate cross-field constraints shared by `--memory` and `--numa`.
102    pub fn validate(&self) -> anyhow::Result<()> {
103        if self.hugepage_size.is_some() && !self.hugepages {
104            anyhow::bail!("hugepage_size requires hugepages=on");
105        }
106        if self.hugepages {
107            if self.shared == Some(false) {
108                anyhow::bail!("hugepages=on conflicts with shared=off");
109            }
110            if self.file.is_some() {
111                anyhow::bail!("hugepages=on conflicts with file=...");
112            }
113        }
114        Ok(())
115    }
116}
117
118/// NUMA node configuration parsed from `--numa`.
119#[derive(Debug, Clone, PartialEq, Eq, vmm_cli::KeyValueArgs)]
120pub struct NumaNodeCli {
121    /// Memory configuration (size, shared, prefetch, hugepages, etc.).
122    #[kv(flatten)]
123    pub memory: MemoryCli,
124    /// Host NUMA node to bind memory allocation to.
125    pub host_numa_node: Option<u32>,
126    /// Explicit VP indices for this node.
127    pub vps: Option<vmm_cli::BracketRangeList>,
128}
129
130/// NUMA distance parsed from `--numa-distance`.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct NumaDistanceCli {
133    /// Source node index.
134    pub src: u32,
135    /// Destination node index.
136    pub dst: u32,
137    /// Distance value (10-255, 255 = unreachable).
138    pub distance: u8,
139}
140
141/// OpenVMM virtual machine monitor.
142///
143/// This is not yet a stable interface and may change radically between
144/// versions.
145#[derive(Parser)]
146pub struct Options {
147    /// processor count
148    #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
149    pub processors: u32,
150
151    /// guest RAM configuration (`SIZE` or `key=value[,key=value...]`)
152    #[clap(
153        short = 'm',
154        long,
155        value_name = "PARAMS",
156        default_value = "1GB",
157        value_parser = parse_memory_config,
158        conflicts_with = "numa",
159        long_help = r#"Configure guest RAM.
160
161Syntax: SIZE | key=value[,key=value...]
162
163Size suffixes accept K, M, G, and T, optionally followed by B.
164
165Options:
166    size=<SIZE>              guest RAM size, default 1GB
167    shared[=on|off]          use shared file-backed RAM, default on
168    prefetch[=on|off]        pre-populate guest RAM mappings
169    thp[=on|off]             mark guest RAM as THP-eligible (Linux), default on
170    hugepages[=on|off]       allocate RAM from hugetlb/large pages (Linux, Windows)
171    hugepage_size=<SIZE>     hugepage size, default 2MB; requires hugepages=on
172    file=<PATH>              use an existing file as guest RAM backing
173
174Examples:
175    --memory 4G
176    --memory size=64GB,hugepages=on,hugepage_size=2MB
177    --memory size=4G,file=path/to/memory.bin
178    --memory size=4G,thp=off"#
179    )]
180    pub memory: MemoryCli,
181
182    /// NUMA node configuration (repeatable, one per node).
183    ///
184    /// Each --numa specifies one guest NUMA node. Mutually exclusive with
185    /// --memory.
186    #[clap(
187        long,
188        value_name = "PARAMS",
189        value_parser = parse_numa_node,
190        conflicts_with = "memory",
191        long_help = r#"Configure a guest NUMA node (repeatable, one per node).
192
193Syntax: key=value[,key=value...]
194
195Options:
196    size=<SIZE>              RAM for this node (required)
197    shared[=on|off]          use shared file-backed RAM, default on
198    prefetch[=on|off]        pre-populate guest RAM mappings
199    thp[=on|off]             mark node RAM as THP-eligible (Linux), default on
200    hugepages[=on|off]       allocate RAM from hugetlb/large pages (Linux, Windows)
201    hugepage_size=<SIZE>     hugepage size, default 2MB; requires hugepages=on
202    host_numa_node=<N>       bind allocation to host NUMA node N
203    vps=<LIST>               explicit VP indices (e.g. "[0,1,2,3]")
204
205  VP lists use bracket syntax with comma-separated indices and dash
206  ranges: vps=[0,1] or vps=[0-3] or vps=[0,1,4-5]. An empty list, vps=[],
207  declares a CPU-less node (e.g. a generic-initiator target); unlike a
208  non-empty list, it may be combined with nodes that omit vps.
209
210Examples:
211    --numa size=2G --numa size=2G
212    --numa size=2G,host_numa_node=0 --numa size=2G,host_numa_node=1
213    --numa size=2G,hugepages=on,vps=[0,1] --numa size=2G,vps=[2,3]
214    --numa size=2G,vps=[0-3] --numa size=2G,vps=[4-7]
215    --numa size=2G --numa size=0,vps=[]"#
216    )]
217    pub numa: Option<Vec<NumaNodeCli>>,
218
219    /// NUMA distance (repeatable). Format: SRC:DST:DISTANCE.
220    ///
221    /// SRC and DST are 0-based node indices. DISTANCE is 10-255 (10 = local, 255 = unreachable).
222    /// Specify each direction explicitly (not auto-symmetric).
223    #[clap(long, value_name = "SRC:DST:DIST", value_parser = parse_numa_distance, conflicts_with = "memory", requires = "numa")]
224    pub numa_distance: Option<Vec<NumaDistanceCli>>,
225
226    /// use shared memory segment
227    #[clap(short = 'M', long, hide = true)]
228    pub shared_memory: bool,
229
230    /// prefetch guest RAM
231    #[clap(long = "prefetch", hide = true, conflicts_with = "numa")]
232    pub deprecated_prefetch: bool,
233
234    /// back guest RAM with a file instead of anonymous memory.
235    /// The file is created/opened and sized to the guest RAM size.
236    /// Enables snapshot save (fsync) and restore (open + mmap).
237    #[clap(
238        long = "memory-backing-file",
239        value_name = "FILE",
240        hide = true,
241        conflicts_with_all = ["deprecated_private_memory", "numa"]
242    )]
243    pub deprecated_memory_backing_file: Option<PathBuf>,
244
245    /// Restore VM from a snapshot directory (implies file-backed memory from
246    /// the snapshot's memory.bin). Cannot be used with --memory-backing-file.
247    #[clap(
248        long,
249        value_name = "DIR",
250        conflicts_with_all = ["deprecated_memory_backing_file", "numa"]
251    )]
252    pub restore_snapshot: Option<PathBuf>,
253
254    /// use private anonymous memory for guest RAM
255    #[clap(long = "private-memory", hide = true, conflicts_with_all = ["deprecated_memory_backing_file", "restore_snapshot", "numa"])]
256    pub deprecated_private_memory: bool,
257
258    /// enable transparent huge pages for guest RAM (Linux only; deprecated, THP is on by default)
259    #[clap(long = "thp", hide = true, conflicts_with = "numa")]
260    pub deprecated_thp: bool,
261
262    /// start in paused state
263    #[clap(short = 'P', long)]
264    pub paused: bool,
265
266    /// kernel image (when using linux direct boot)
267    #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
268    pub kernel: OptionalPathBuf,
269
270    /// initrd image (when using linux direct boot)
271    #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
272    pub initrd: OptionalPathBuf,
273
274    /// extra kernel command line args
275    #[clap(short = 'c', long, value_name = "STRING")]
276    pub cmdline: Vec<String>,
277
278    /// enable HV#1 capabilities
279    #[clap(long)]
280    pub hv: bool,
281
282    /// Use a full device tree instead of ACPI tables for ARM64 Linux direct
283    /// boot. By default, ARM64 uses ACPI mode (stub DT + EFI + ACPI tables).
284    /// This flag selects the legacy DT-only path. Rejected on x86.
285    #[clap(long, conflicts_with_all = ["uefi", "pcat", "igvm"])]
286    pub device_tree: bool,
287
288    /// enable vtl2 - only supported in WHP and simulated without hypervisor support currently
289    ///
290    /// Currently implies --get.
291    #[clap(long, requires("hv"))]
292    pub vtl2: bool,
293
294    /// Add GET and related devices for using the OpenHCL paravisor to the
295    /// highest enabled VTL.
296    #[clap(long, requires("hv"))]
297    pub get: bool,
298
299    /// Disable GET and related devices for using the OpenHCL paravisor, even
300    /// when --vtl2 is passed.
301    #[clap(long, conflicts_with("get"))]
302    pub no_get: bool,
303
304    /// Run without VMBus, even if --hv or --uefi are specified.
305    #[clap(
306        long,
307        conflicts_with_all = [
308            "vmbus_vsock_path",
309            "vmbus_vtl2_vsock_path",
310            "vmbus_redirect",
311            "vmbus_max_version",
312            "vmbus_com1_serial",
313            "vmbus_com2_serial",
314            "vtl2",
315            "get",
316            "pcat",
317        ],
318    )]
319    pub no_vmbus: bool,
320
321    /// disable the VTL0 alias map presented to VTL2 by default
322    #[clap(long, requires("vtl2"))]
323    pub no_alias_map: bool,
324
325    /// enable isolation emulation
326    #[clap(long, requires("vtl2"))]
327    pub isolation: Option<IsolationCli>,
328
329    /// the hybrid vsock listener path
330    #[clap(long, value_name = "PATH", alias = "vsock-path")]
331    pub vmbus_vsock_path: Option<String>,
332
333    /// the VTL2 hybrid vsock listener path
334    #[clap(long, value_name = "PATH", requires("vtl2"), alias = "vtl2-vsock-path")]
335    pub vmbus_vtl2_vsock_path: Option<String>,
336
337    /// the late map vtl0 ram access policy when vtl2 is enabled
338    #[clap(long, requires("vtl2"), default_value = "halt")]
339    pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
340
341    /// attach a disk (can be passed multiple times)
342    #[clap(long_help = r#"
343e.g: --disk memdiff:file:/path/to/disk.vhd
344
345syntax: <path> | kind:<arg>[,flag,opt=arg,...]
346
347valid disk kinds:
348    `mem:<len>`                    memory backed disk
349        <len>: length of ramdisk, e.g.: `1G`
350    `memdiff:<disk>`               memory backed diff disk
351        <disk>: lower disk, e.g.: `file:base.img`
352    `file:<path>[;direct][;create=<len>]`   file-backed disk
353        <path>: path to file
354        `;direct`: bypass the OS page cache
355    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
356    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
357    `autocache:<key>:<disk>`       auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
358    `blob:<type>:<url>`            HTTP blob (read-only)
359        <type>: `flat` or `vhd1`
360    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
361        <cipher>: `xts-aes-256`
362    `prwrap:<disk>`                persistent reservations wrapper
363
364flags:
365    `ro`                           open disk as read-only
366    `dvd`                          specifies that device is cd/dvd and it is read_only
367    `vtl2`                         assign this disk to VTL2
368    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
369    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
370
371options:
372    `pcie_port=<name>`             present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
373    `on=<name>`                    attach to a named controller (NVMe or SCSI), incompatible with `pcie_port` and `vtl2`
374    `nsid=<N>`                     NVMe namespace ID (1-based), requires `on`; auto-assigned if omitted
375    `lun=<N>`                      SCSI LUN (0-based), requires `on`; auto-assigned if omitted
376    `relay=<ctrl>[:<loc>]`         relay through OpenHCL to the named OpenHCL controller, with optional location (LUN or NSID)
377"#)]
378    #[clap(long, value_name = "FILE")]
379    pub disk: Vec<DiskCli>,
380
381    /// \[deprecated\] attach a disk via an NVMe controller
382    ///
383    /// Use --nvme-pci and --disk on=\<name\> instead.
384    #[clap(long_help = r#"
385e.g: --nvme memdiff:file:/path/to/disk.vhd
386
387syntax: <path> | kind:<arg>[,flag,opt=arg,...]
388
389valid disk kinds:
390    `mem:<len>`                    memory backed disk
391        <len>: length of ramdisk, e.g.: `1G`
392    `memdiff:<disk>`               memory backed diff disk
393        <disk>: lower disk, e.g.: `file:base.img`
394    `file:<path>[;direct][;create=<len>]`   file-backed disk
395        <path>: path to file
396        `;direct`: bypass the OS page cache
397    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
398    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
399    `autocache:<key>:<disk>`       auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
400    `blob:<type>:<url>`            HTTP blob (read-only)
401        <type>: `flat` or `vhd1`
402    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
403        <cipher>: `xts-aes-256`
404    `prwrap:<disk>`                persistent reservations wrapper
405
406flags:
407    `ro`                           open disk as read-only
408    `vtl2`                         assign this disk to VTL2
409    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
410    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
411
412options:
413    `pcie_port=<name>`             present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
414"#)]
415    #[clap(long)]
416    pub nvme: Vec<DiskCli>,
417
418    /// create a named NVMe controller
419    #[clap(long_help = r#"
420Create a named NVMe controller with an explicit transport.
421
422syntax: id=<name>,pcie_port=<port> | id=<name>,vpci[=<guid>]
423
424The controller name can be referenced by `--disk` with the `on=<name>`
425option to attach namespaces to this controller.
426
427options:
428    `id=<name>`                    controller name (required)
429    `pcie_port=<port>`             present on PCIe under the specified port
430    `vpci[=<guid>]`                present via VPCI; optional instance GUID
431    `vtl2`                         assign to VTL2 (default VTL0)
432
433Exactly one of `pcie_port` or `vpci` must be specified.
434
435Examples:
436    --nvme-pci id=nvme0,pcie_port=p0
437    --nvme-pci id=nvme1,vpci
438    --nvme-pci id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c
439"#)]
440    #[clap(long = "nvme-pci")]
441    pub nvme_pci: Vec<NvmeControllerCli>,
442
443    /// create a named VMBus SCSI controller
444    #[clap(long_help = r#"
445Create a named VMBus SCSI controller.
446
447syntax: id=<name>[,sub_channels=<N>][,vtl2]
448
449The controller name can be referenced by `--disk` with the `on=<name>`
450option to attach disks to this controller.
451
452options:
453    `id=<name>`                    controller name (required)
454    `sub_channels=<N>`             number of sub-channels (default 0)
455    `vtl2`                         assign to VTL2 (default VTL0)
456
457Examples:
458    --vmbus-scsi id=scsi0
459    --vmbus-scsi id=scsi1,sub_channels=4
460"#)]
461    #[clap(long = "vmbus-scsi")]
462    pub vmbus_scsi: Vec<ScsiControllerCli>,
463
464    /// register an OpenHCL-managed storage controller (relay target)
465    #[clap(long_help = r#"
466Register an OpenHCL-managed storage controller that can be used as a
467relay target with `--disk ... relay=<name>`.
468
469syntax: id=<name>,type=scsi|nvme[,guid=<guid>]
470
471options:
472    `id=<name>`                    controller name (required)
473    `type=scsi|nvme`               controller protocol (required)
474    `guid=<guid>`                  instance GUID (auto-derived from name if omitted)
475
476Examples:
477    --openhcl-controller id=vtl0-scsi,type=scsi
478    --openhcl-controller id=vtl0-nvme,type=nvme,guid=09a59b81-...
479"#)]
480    #[clap(long = "openhcl-controller")]
481    pub openhcl_controller: Vec<OpenhclControllerCli>,
482
483    /// attach a CXL Type-3 test endpoint on a PCIe root port
484    #[clap(long = "cxl-test", value_name = "mem:<len>,pcie_port=<name>")]
485    pub cxl_test: Vec<CxlTestDeviceCli>,
486
487    /// attach a disk via a virtio-blk controller
488    #[clap(long_help = r#"
489e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
490
491syntax: <path> | kind:<arg>[,flag,opt=arg,...]
492
493valid disk kinds:
494    `mem:<len>`                    memory backed disk
495        <len>: length of ramdisk, e.g.: `1G`
496    `memdiff:<disk>`               memory backed diff disk
497        <disk>: lower disk, e.g.: `file:base.img`
498    `file:<path>[;direct]`                  file-backed disk
499        <path>: path to file
500        `;direct`: bypass the OS page cache
501
502flags:
503    `ro`                           open disk as read-only
504
505options:
506    `pcie_port=<name>`             present the disk using pcie under the specified port
507"#)]
508    #[clap(long = "virtio-blk")]
509    pub virtio_blk: Vec<DiskCli>,
510
511    /// Attach a vhost-user device via a Unix socket.
512    ///
513    /// The first positional argument is the socket path. Options:
514    ///
515    /// ```text
516    ///   type=blk|fs                        — device type (shorthand)
517    ///   device_id=N                        — numeric virtio device ID
518    ///   tag=NAME                           — mount tag (required for type=fs)
519    ///   num_queues=N                       — queue count (type=blk/fs only)
520    ///   queue_size=N                       — per-queue size (type=blk/fs only)
521    ///   queue_sizes=[N,N,N]                — per-queue sizes (device_id= only)
522    ///   pcie_port=NAME                     — present on PCIe under the specified port
523    /// ```
524    ///
525    /// Examples:
526    ///
527    /// ```text
528    ///   --vhost-user /tmp/vhost.sock,type=blk
529    ///   --vhost-user /tmp/vhost.sock,type=blk,num_queues=4,queue_size=512
530    ///   --vhost-user /tmp/vhost.sock,device_id=2,queue_sizes=[128,128]
531    ///   --vhost-user /tmp/vhost.sock,type=blk,pcie_port=port0
532    ///   --vhost-user /tmp/virtiofsd.sock,type=fs,tag=myfs
533    ///   --vhost-user /tmp/virtiofsd.sock,type=fs,tag=myfs,num_queues=2,queue_size=1024
534    /// ```
535    #[cfg(target_os = "linux")]
536    #[clap(long = "vhost-user")]
537    pub vhost_user: Vec<VhostUserCli>,
538
539    /// number of sub-channels for the SCSI controller
540    #[clap(long, value_name = "COUNT", default_value = "0")]
541    pub scsi_sub_channels: u16,
542
543    /// expose a virtual NIC
544    #[clap(long)]
545    pub nic: bool,
546
547    /// expose a virtual NIC with the given backend (consomme | dio | tap | none)
548    ///
549    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
550    /// `vtl2:` to assign this NIC to VTL2, or `pcie_port=<port_name>:` to
551    /// expose the NIC over emulated PCIe at the specified port.
552    ///
553    /// For consomme, forward host ports into the guest with `hostfwd=`:
554    ///   --net consomme:hostfwd=tcp::3389-:3389
555    ///   --net consomme:hostfwd=tcp:127.0.0.1:8080-:80
556    ///   --net consomme:hostfwd=tcp:\[::1\]:8080-:80
557    ///   --net consomme:10.0.0.0/24,hostfwd=tcp::22-:22,hostfwd=udp::5000-:5000
558    #[clap(long)]
559    pub net: Vec<NicConfigCli>,
560
561    /// expose a virtual NIC using the Windows kernel-mode vmswitch.
562    ///
563    /// Specify the switch ID or "default" for the default switch.
564    #[clap(long, value_name = "SWITCH_ID")]
565    pub kernel_vmnic: Vec<String>,
566
567    /// expose a graphics device
568    #[clap(long)]
569    pub gfx: bool,
570
571    /// support a graphics device in vtl2
572    #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
573    pub vtl2_gfx: bool,
574
575    /// VNC server configuration (listen address, port, client limit, etc.).
576    #[clap(flatten)]
577    pub vnc: VncCli,
578
579    /// set the APIC ID offset, for testing APIC IDs that don't match VP index
580    #[cfg(guest_arch = "x86_64")]
581    #[clap(long, default_value_t)]
582    pub apic_id_offset: u32,
583
584    /// the maximum number of VPs per socket
585    #[clap(long)]
586    pub vps_per_socket: Option<u32>,
587
588    /// enable or disable SMT (hyperthreading) (auto | force | off)
589    #[clap(long, default_value = "auto")]
590    pub smt: SmtConfigCli,
591
592    /// configure x2apic (auto | supported | off | on)
593    #[cfg(guest_arch = "x86_64")]
594    #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
595    pub x2apic: X2ApicConfig,
596
597    /// configure PCIe MSI controller for aarch64 (auto | its | v2m)
598    #[cfg(guest_arch = "aarch64")]
599    #[clap(long, default_value = "auto")]
600    pub gic_msi: GicMsiCli,
601
602    /// configure SMMUv3 IOMMU for an aarch64 PCIe root complex (repeatable).
603    ///
604    /// Syntax: `rc=<name>[,accel][,oas=auto|N]`.
605    #[cfg(guest_arch = "aarch64")]
606    #[clap(long, value_name = "SMMU_CONFIG")]
607    pub smmu: Vec<SmmuCli>,
608
609    /// COM1 binding, optionally prefixed with `debugger-mode:` (see below)
610    /// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
611    ///
612    /// Prefix the binding with `debugger-mode:` to run this COM port in
613    /// debugger mode for WinDbg kernel debugging over serial (KD), e.g.
614    /// `--com1 debugger-mode:listen=<path>` or
615    /// `--com1 debugger-mode:listen=tcp:<ip>:<port>`. In debugger mode OpenVMM
616    /// keeps this port's backend drained and may drop bytes instead of applying
617    /// backpressure, so the KD transport does not deadlock across guest
618    /// resets/reboots (KD recovers dropped bytes via its own retransmission).
619    /// Debugger mode is independent per COM port.
620    #[clap(long, value_name = "SERIAL")]
621    pub com1: Option<ComSerialConfigCli>,
622
623    /// COM2 binding, optionally prefixed with `debugger-mode:` (see --com1)
624    /// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
625    #[clap(long, value_name = "SERIAL")]
626    pub com2: Option<ComSerialConfigCli>,
627
628    /// COM3 binding, optionally prefixed with `debugger-mode:` (see --com1)
629    /// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
630    #[clap(long, value_name = "SERIAL")]
631    pub com3: Option<ComSerialConfigCli>,
632
633    /// COM4 binding, optionally prefixed with `debugger-mode:` (see --com1)
634    /// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
635    #[clap(long, value_name = "SERIAL")]
636    pub com4: Option<ComSerialConfigCli>,
637
638    /// vmbus com1 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
639    #[structopt(long, value_name = "SERIAL")]
640    pub vmbus_com1_serial: Option<SerialConfigCli>,
641
642    /// vmbus com2 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
643    #[structopt(long, value_name = "SERIAL")]
644    pub vmbus_com2_serial: Option<SerialConfigCli>,
645
646    /// Only allow guest to host serial traffic
647    #[clap(long)]
648    pub serial_tx_only: bool,
649
650    /// debugcon binding (port:serial, where port is a u16, and serial is (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none))
651    #[clap(long, value_name = "SERIAL")]
652    pub debugcon: Option<DebugconSerialConfigCli>,
653
654    /// boot UEFI firmware
655    #[clap(long, short = 'e')]
656    pub uefi: bool,
657
658    /// UEFI firmware file
659    #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
660    pub uefi_firmware: OptionalPathBuf,
661
662    /// enable UEFI debugging on COM1
663    #[clap(long, requires("uefi"))]
664    pub uefi_debug: bool,
665
666    /// enable memory protections in UEFI
667    #[clap(long, requires("uefi"))]
668    pub uefi_enable_memory_protections: bool,
669
670    /// force UEFI to bounce-buffer all DMA traffic
671    #[clap(long, requires("uefi"))]
672    pub uefi_force_dma_bounce: bool,
673
674    /// set PCAT boot order as comma-separated string of boot device types
675    /// (e.g: floppy,hdd,optical,net).
676    ///
677    /// If less than 4 entries are added, entries are added according to their
678    /// default boot order (optical,hdd,net,floppy)
679    ///
680    /// e.g: passing "floppy,optical" will result in a boot order equivalent to
681    /// "floppy,optical,hdd,net".
682    ///
683    /// Passing duplicate types is an error.
684    #[clap(long, requires("pcat"))]
685    pub pcat_boot_order: Option<PcatBootOrderCli>,
686
687    /// Boot with PCAT BIOS firmware and piix4 devices
688    #[clap(long, conflicts_with("uefi"))]
689    pub pcat: bool,
690
691    /// PCAT firmware file
692    #[clap(long, requires("pcat"), value_name = "FILE")]
693    pub pcat_firmware: Option<PathBuf>,
694
695    /// boot IGVM file
696    #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
697    pub igvm: Option<PathBuf>,
698
699    /// specify igvm vtl2 relocation type
700    /// (absolute=\<addr\>, disable, auto=\<filesize,or memory size\>, vtl2=\<filesize,or memory size\>,)
701    #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
702    pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
703
704    /// add a virtio_9p device (e.g. myfs,C:\)
705    ///
706    /// Prefix with `pcie_port=<port_name>:` to expose the device over
707    /// emulated PCIe at the specified port.
708    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
709    pub virtio_9p: Vec<FsArgs>,
710
711    /// output debug info from the 9p server
712    #[clap(long)]
713    pub virtio_9p_debug: bool,
714
715    /// add a virtio_fs device (e.g. myfs,C:\,uid=1000,gid=2000)
716    ///
717    /// Prefix with `pcie_port=<port_name>:` to expose the device over
718    /// emulated PCIe at the specified port.
719    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
720    pub virtio_fs: Vec<FsArgsWithOptions>,
721
722    /// add a virtio_fs device for sharing memory (e.g. myfs,\SectionDirectoryPath)
723    ///
724    /// Prefix with `pcie_port=<port_name>:` to expose the device over
725    /// emulated PCIe at the specified port.
726    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
727    pub virtio_fs_shmem: Vec<FsArgs>,
728
729    /// add a virtio_fs device under either the PCI or MMIO bus, or whatever the hypervisor supports (pci | mmio | auto)
730    #[clap(long, value_name = "BUS", default_value = "auto")]
731    pub virtio_fs_bus: VirtioBusCli,
732
733    /// virtio PMEM device
734    ///
735    /// Prefix with `pcie_port=<port_name>:` to expose the device over
736    /// emulated PCIe at the specified port.
737    #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
738    pub virtio_pmem: Option<VirtioPmemArgs>,
739
740    /// add a virtio entropy (RNG) device
741    #[clap(long)]
742    pub virtio_rng: bool,
743
744    /// add a virtio-rng device under either the PCI or MMIO bus, or whatever the hypervisor supports (pci | mmio | vpci | auto)
745    #[clap(long, value_name = "BUS", default_value = "auto")]
746    pub virtio_rng_bus: VirtioBusCli,
747
748    /// attach the virtio-rng device to the specified PCIe port (overrides --virtio-rng-bus)
749    #[clap(long, value_name = "PORT", requires("virtio_rng"))]
750    pub virtio_rng_pcie_port: Option<String>,
751
752    /// virtio console device backed by a serial backend (/dev/hvc0 in guest)
753    ///
754    /// Accepts serial config (console | stderr | listen=\<path\> |
755    /// file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> |
756    /// term[=\<program\>]\[,name=\<windowtitle\>\] | none)
757    #[clap(long)]
758    pub virtio_console: Option<SerialConfigCli>,
759
760    /// attach the virtio-console device to the specified PCIe port
761    #[clap(long, value_name = "PORT", requires("virtio_console"))]
762    pub virtio_console_pcie_port: Option<String>,
763
764    /// add a virtio vsock device with the given Unix socket base path
765    #[clap(long, value_name = "PATH")]
766    pub virtio_vsock_path: Option<String>,
767
768    /// expose the guest in the host AF_VSOCK namespace using the Linux
769    /// vhost_vsock kernel backend
770    #[cfg(target_os = "linux")]
771    #[clap(
772        long,
773        value_name = "CID",
774        conflicts_with = "virtio_vsock_path",
775        value_parser = parse_vhost_vsock_cid
776    )]
777    pub virtio_vsock_vhost_cid: Option<u32>,
778
779    /// expose a virtio network with the given backend (dio | vmnic | tap |
780    /// none)
781    ///
782    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
783    /// `vtl2:` to assign this NIC to VTL2, or `pcie_port=<port_name>:` to
784    /// expose the NIC over emulated PCIe at the specified port.
785    #[clap(long)]
786    pub virtio_net: Vec<NicConfigCli>,
787
788    /// send log output from the worker process to a file instead of stderr. the file will be overwritten.
789    #[clap(long, value_name = "PATH")]
790    pub log_file: Option<PathBuf>,
791
792    /// write the process ID to the specified file on startup, and remove it on
793    /// exit. the file is not removed if the process is killed with SIGKILL or
794    /// crashes. no file locking is performed.
795    #[clap(long, value_name = "PATH")]
796    pub pidfile: Option<PathBuf>,
797
798    /// \[deprecated\] run as a ttrpc server on the specified Unix socket
799    ///
800    /// Use `--rpc path=<PATH>,transport=ttrpc` instead.
801    #[clap(long, value_name = "SOCKETPATH")]
802    pub ttrpc: Option<PathBuf>,
803
804    /// \[deprecated\] run as a grpc server on the specified Unix socket
805    ///
806    /// Use `--rpc path=<PATH>,transport=grpc` instead.
807    #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
808    pub grpc: Option<PathBuf>,
809
810    /// run as an RPC server on the specified Unix socket
811    #[clap(long_help = r#"
812Run as an RPC server on the specified Unix socket.
813
814syntax: path=<PATH>[,transport=<TRANSPORT>]
815
816options:
817    `path=<PATH>`                  Unix socket path to listen on (required)
818    `transport=<TRANSPORT>`        wire transport to accept (default: auto)
819
820valid transports:
821    `auto`                         auto-detect ttrpc vs. gRPC per connection
822    `ttrpc`                        accept ttrpc clients only
823    `grpc`                         accept gRPC clients only
824
825Examples:
826    --rpc path=/tmp/openvmm.sock
827    --rpc path=/tmp/openvmm.sock,transport=ttrpc
828"#)]
829    #[clap(
830        long,
831        value_name = "path=PATH[,transport=TRANSPORT]",
832        conflicts_with("ttrpc"),
833        conflicts_with("grpc")
834    )]
835    pub rpc: Option<RpcCli>,
836
837    /// do not launch child processes
838    #[clap(long)]
839    pub single_process: bool,
840
841    /// device to assign (can be passed multiple times)
842    #[cfg(windows)]
843    #[clap(long, value_name = "PATH")]
844    pub device: Vec<String>,
845
846    /// instead of showing the frontpage the VM will shutdown instead
847    #[clap(long, requires("uefi"))]
848    pub disable_frontpage: bool,
849
850    /// add a vtpm device
851    #[clap(long)]
852    pub tpm: bool,
853
854    /// the mesh worker host name.
855    ///
856    /// Used internally for debugging and diagnostics.
857    #[clap(long, default_value = "control", hide(true))]
858    #[expect(clippy::option_option)]
859    pub internal_worker: Option<Option<String>>,
860
861    /// redirect the VTL 0 vmbus control plane to a proxy in VTL 2.
862    #[clap(long, requires("vtl2"))]
863    pub vmbus_redirect: bool,
864
865    /// limit the maximum protocol version allowed by vmbus; used for testing purposes
866    #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
867    pub vmbus_max_version: Option<u32>,
868
869    /// The disk to use for the VMGS.
870    ///
871    /// If this is not provided, guest state will be stored in memory.
872    #[clap(long_help = r#"
873e.g: --vmgs memdiff:file:/path/to/file.vmgs
874
875syntax: <path> | kind:<arg>[,flag]
876
877valid disk kinds:
878    `mem:<len>`                     memory backed disk
879        <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
880    `memdiff:<disk>[;create=<len>]` memory backed diff disk
881        <disk>: lower disk, e.g.: `file:base.img`
882    `file:<path>`                   file-backed disk
883        <path>: path to file
884
885flags:
886    `fmt`                           reprovision the VMGS before boot
887    `fmt-on-fail`                   reprovision the VMGS before boot if it is corrupted
888"#)]
889    #[clap(long)]
890    pub vmgs: Option<VmgsCli>,
891
892    /// Use GspById guest state encryption policy with a test seed
893    #[clap(long, requires("vmgs"))]
894    pub test_gsp_by_id: bool,
895
896    /// VGA firmware file
897    #[clap(long, requires("pcat"), value_name = "FILE")]
898    pub vga_firmware: Option<PathBuf>,
899
900    /// enable secure boot
901    #[clap(long)]
902    pub secure_boot: bool,
903
904    /// use secure boot template
905    #[clap(long)]
906    pub secure_boot_template: Option<SecureBootTemplateCli>,
907
908    /// custom uefi nvram json file
909    #[clap(long, value_name = "PATH")]
910    pub custom_uefi_json: Option<PathBuf>,
911
912    /// the path to a named pipe (Windows) or Unix socket (Linux) to relay to the connected
913    /// tty.
914    ///
915    /// This is a hidden argument used internally.
916    #[clap(long, hide(true))]
917    pub relay_console_path: Option<PathBuf>,
918
919    /// the title of the console window spawned from the relay console.
920    ///
921    /// This is a hidden argument used internally.
922    #[clap(long, hide(true))]
923    pub relay_console_title: Option<String>,
924
925    /// enable in-hypervisor gdb debugger
926    #[clap(long, value_name = "PORT")]
927    pub gdb: Option<u16>,
928
929    /// enable emulated MANA devices with the given network backend (see --net)
930    ///
931    /// Prefix with `pcie_port=<port_name>:` to expose the nic over emulated PCIe
932    /// at the specified port.
933    #[clap(long)]
934    pub mana: Vec<NicConfigCli>,
935
936    /// use a specific hypervisor interface, with optional backend-specific
937    /// parameters.
938    ///
939    /// Format: `name` or `name:key=val,key,...`
940    ///
941    /// WHP parameters (x86_64 guests only):
942    ///   user_mode_apic       - use user-mode APIC emulator
943    ///   no_enlightenments    - disable in-hypervisor enlightenments
944    ///
945    /// Examples:
946    ///   --hypervisor whp
947    ///   --hypervisor whp:user_mode_apic
948    ///   --hypervisor whp:user_mode_apic,no_enlightenments
949    ///   --hypervisor kvm
950    #[clap(long)]
951    pub hypervisor: Option<String>,
952
953    /// expose hardware virtualization (VMX/SVM) to the guest so it can run its
954    /// own hypervisor.
955    ///
956    /// Only supported on x86_64, and only by backends that support nested
957    /// virtualization (currently WHP and KVM). Requires host support.
958    #[clap(long)]
959    pub nested_virt: bool,
960
961    /// attach an ide drive (can be passed multiple times)
962    ///
963    /// Each ide controller has two channels. Each channel can have up to two
964    /// attachments.
965    ///
966    /// If the `s` flag is not passed then the drive will we be attached to the
967    /// primary ide channel if space is available. If two attachments have already
968    /// been added to the primary channel then the drive will be attached to the
969    /// secondary channel.
970    #[clap(long_help = r#"
971e.g: --ide memdiff:file:/path/to/disk.vhd
972
973syntax: <path> | kind:<arg>[,flag,opt=arg,...]
974
975valid disk kinds:
976    `mem:<len>`                    memory backed disk
977        <len>: length of ramdisk, e.g.: `1G`
978    `memdiff:<disk>`               memory backed diff disk
979        <disk>: lower disk, e.g.: `file:base.img`
980    `file:<path>[;create=<len>]`   file-backed disk
981        <path>: path to file
982    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
983    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
984    `blob:<type>:<url>`            HTTP blob (read-only)
985        <type>: `flat` or `vhd1`
986    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
987        <cipher>: `xts-aes-256`
988
989additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
990this list is not exhaustive.
991
992flags:
993    `ro`                           open disk as read-only
994    `s`                            attach drive to secondary ide channel
995    `dvd`                          specifies that device is cd/dvd and it is read_only
996"#)]
997    #[clap(long, value_name = "FILE", requires("pcat"))]
998    pub ide: Vec<IdeDiskCli>,
999
1000    /// attach a floppy drive (should be able to be passed multiple times). VM must be generation 1 (no UEFI)
1001    ///
1002    #[clap(long_help = r#"
1003e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
1004
1005syntax: <path> | kind:<arg>[,flag,opt=arg,...]
1006
1007valid disk kinds:
1008    `mem:<len>`                    memory backed disk
1009        <len>: length of ramdisk, e.g.: `1G`
1010    `memdiff:<disk>`               memory backed diff disk
1011        <disk>: lower disk, e.g.: `file:base.img`
1012    `file:<path>[;create=<len>]`   file-backed disk
1013        <path>: path to file
1014    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
1015    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
1016    `blob:<type>:<url>`            HTTP blob (read-only)
1017        <type>: `flat` or `vhd1`
1018    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
1019        <cipher>: `xts-aes-256`
1020
1021flags:
1022    `ro`                           open disk as read-only
1023"#)]
1024    #[clap(long, value_name = "FILE", requires("pcat"))]
1025    pub floppy: Vec<FloppyDiskCli>,
1026
1027    /// enable guest watchdog device
1028    #[clap(long)]
1029    pub guest_watchdog: bool,
1030
1031    /// Enable OpenHCL's crash dump device, writing ELF core dumps of
1032    /// VTL2 user-mode components of OpenHCL in the given directory.
1033    #[clap(long)]
1034    pub openhcl_dump_path: Option<PathBuf>,
1035
1036    /// what to do when the guest requests a reset: reset it (default), halt the
1037    /// VM for inspection, or exit the VMM process (use `exit:<code>` to set the
1038    /// exit status)
1039    #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action)]
1040    pub guest_reset_action: GuestPowerAction,
1041
1042    /// what to do when the guest powers off or hibernates: halt the VM for
1043    /// inspection (default), reset it, or exit the VMM process (use
1044    /// `exit:<code>` to set the exit status)
1045    #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1046    pub guest_shutdown_action: GuestPowerAction,
1047
1048    /// what to do when the guest triple-faults: halt the VM for inspection
1049    /// (default), reset it, or exit the VMM process (use `exit:<code>` to set
1050    /// the exit status)
1051    #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1052    pub guest_crash_action: GuestPowerAction,
1053
1054    /// when the guest triple-faults, write a WinDbg-compatible `.vmrs` dump of
1055    /// the whole VM's VP state and guest memory to the specified path before
1056    /// applying the crash action
1057    ///
1058    /// This is a host-side, whole-VM dump triggered by a triple fault, distinct
1059    /// from `--openhcl-dump-path` (which captures an ELF core dump of user-mode
1060    /// components in OpenHCL).
1061    #[clap(long, value_name = "PATH")]
1062    pub crash_dump_path: Option<PathBuf>,
1063
1064    /// what to do when the guest watchdog fires (the guest stopped petting it):
1065    /// reset the VM (default), halt it for inspection, or exit the VMM process
1066    /// (use `exit:<code>` to set the exit status). Requires `--guest-watchdog`.
1067    #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action, requires = "guest_watchdog")]
1068    pub guest_watchdog_action: GuestPowerAction,
1069
1070    /// write saved state .proto files to the specified path
1071    #[clap(long)]
1072    pub write_saved_state_proto: Option<PathBuf>,
1073
1074    /// specify the IMC hive file for booting Windows
1075    #[clap(long)]
1076    pub imc: Option<PathBuf>,
1077
1078    /// expose a battery device
1079    #[clap(long)]
1080    pub battery: bool,
1081
1082    /// set the uefi console mode
1083    #[clap(long)]
1084    pub uefi_console_mode: Option<UefiConsoleModeCli>,
1085
1086    /// set the EFI diagnostics log level
1087    #[clap(long_help = r#"
1088Set the EFI diagnostics log level.
1089
1090options:
1091    default                        default (ERROR and WARN only)
1092    info                           info (ERROR, WARN, and INFO)
1093    full                           full (all log levels)
1094"#)]
1095    #[clap(long, requires("uefi"))]
1096    pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
1097
1098    /// Perform a default boot even if boot entries exist and fail
1099    #[clap(long)]
1100    pub default_boot_always_attempt: bool,
1101
1102    /// Enable AMD IOMMU (AMD-Vi) emulation on specified root complexes.
1103    /// Repeat for each root complex that should have an IOMMU, e.g.:
1104    ///   --amd-iommu rc0 --amd-iommu rc1
1105    /// The IOMMU appears at device 0 function 0 on each specified root
1106    /// complex. Requires --pcie-root-complex.
1107    #[cfg(guest_arch = "x86_64")]
1108    #[clap(long)]
1109    pub amd_iommu: Vec<String>,
1110
1111    /// Enable Intel VT-d IOMMU emulation on specified root complexes.
1112    /// Repeat for each root complex that should have an IOMMU, e.g.:
1113    ///   --intel-vtd rc0 --intel-vtd rc1
1114    /// Mutually exclusive with --amd-iommu within the same VM.
1115    /// Requires --pcie-root-complex.
1116    #[cfg(guest_arch = "x86_64")]
1117    #[clap(long)]
1118    pub intel_vtd: Vec<String>,
1119
1120    /// Attach a PCI Express root complex to the VM
1121    #[clap(long_help = r#"
1122Attach root complexes to the VM.
1123
1124Examples:
1125    # Attach root complex rc0 on segment 0 with bus and MMIO ranges
1126    --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
1127
1128    # Configure HDM window size and restrictions (bitmask)
1129    --pcie-root-complex rc1,hdm=2G,hdm_window_restrictions=0x21
1130
1131Syntax: <name>[,opt=arg,...]
1132
1133Options:
1134    `segment=<value>`              configures the PCI Express segment, default 0
1135    `start_bus=<value>`            lowest valid bus number, default 0
1136    `end_bus=<value>`              highest valid bus number, default 255
1137    `low_mmio=<size>`              low MMIO window size, default 64M
1138    `high_mmio=<size>`             high MMIO window size, default 1G
1139    `low_mmio_base=<addr>`         pin low MMIO window base address (0x-prefixed hex)
1140    `high_mmio_base=<addr>`        pin high MMIO window base address (0x-prefixed hex)
1141    `hdm=<size>`                   HDM decoder MMIO window size (CFMWS window), default 1G
1142    `hdm_window_restrictions=<m>`  CFMWS window restriction bitmask (u16, decimal or 0x-prefixed hex),
1143                                   default DEVICE_COHERENT (bit 0, value 0x1)
1144    `preserve_bars`                keep pinned BARs at their assigned addresses
1145    `node=<value>`                 NUMA node the root complex is associated with
1146"#)]
1147    #[clap(long, conflicts_with("pcat"))]
1148    pub pcie_root_complex: Vec<PcieRootComplexCli>,
1149
1150    /// Attach a PCI Express root port to the VM
1151    #[clap(long_help = r#"
1152Attach root ports to root complexes.
1153
1154Examples:
1155    # Attach root port rc0rp0 to root complex rc0
1156    --pcie-root-port rc0:rc0rp0
1157
1158    # Attach root port rc0rp1 to root complex rc0 with hotplug support
1159    --pcie-root-port rc0:rc0rp1,hotplug
1160
1161    # Attach root port rc0rp2 at device 5, function 0
1162    --pcie-root-port rc0:rc0rp2,addr=5
1163
1164    # Attach root port rc0rp3 at device 5, function 1
1165    --pcie-root-port rc0:rc0rp3,addr=5.1
1166
1167Syntax: <root_complex_name>:<name>[,opt,opt=arg,...]
1168
1169Options:
1170    `addr=<dev>[.<fn>]`            device/function to place this port at (default:
1171                                   lowest available); dev 0-31, fn 0-7
1172    `hotplug`                      enable hotplug support for this root port
1173    `acs=<mask>`                   ACS capability bitmask (u16, decimal or 0x-prefixed hex)
1174    `cxl`                          configure this root port as CXL-capable
1175    `pasid`                        configure this port to support PASID for downstream devices
1176"#)]
1177    #[clap(long, conflicts_with("pcat"))]
1178    pub pcie_root_port: Vec<PcieRootPortCli>,
1179
1180    /// Attach a PCI Express switch to the VM
1181    #[clap(long_help = r#"
1182Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
1183
1184Examples:
1185    # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
1186    --pcie-switch rp0:switch0,num_downstream_ports=4
1187
1188    # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
1189    --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
1190
1191    # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
1192    --pcie-switch rp0:switch0
1193    --pcie-switch switch0-downstream-0:switch1
1194    --pcie-switch switch1-downstream-1:switch2
1195
1196    # Enable hotplug on all downstream switch ports of switch0
1197    --pcie-switch rp0:switch0,hotplug
1198
1199    # Enable PASID on all downstream switch ports of switch0
1200    --pcie-switch rp0:switch0,pasid
1201
1202Syntax: <port_name>:<name>[,opt,opt=arg,...]
1203
1204    port_name can be:
1205        - Root port name (e.g., "rp0") to connect directly to a root port
1206        - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
1207
1208Options:
1209    `hotplug`                       enable hotplug support for all downstream switch ports
1210    `num_downstream_ports=<value>`  number of downstream ports, default 4
1211    `acs=<mask>`                    ACS capability bitmask for downstream switch ports
1212    `pasid`                         configure this port to support PASID for downstream devices
1213"#)]
1214    #[clap(long, conflicts_with("pcat"))]
1215    pub pcie_switch: Vec<GenericPcieSwitchCli>,
1216
1217    /// Declare the device behind a PCIe port as an SRAT generic initiator
1218    #[clap(long_help = r#"
1219Declare that the device directly behind a PCIe port is a generic initiator
1220(GI) for a NUMA node, generating an SRAT Generic Initiator Affinity structure.
1221
1222The port may be a root port or a switch downstream port, so this works for
1223devices that sit behind a switch (e.g. a GPU placed under a switch shared
1224with a NIC for peer-to-peer DMA). The port is resolved by name against the
1225live topology after switch downstream ports are enumerated.
1226
1227Examples:
1228    # The device behind switch downstream port sw1-downstream-0 is a generic
1229    # initiator for NUMA node 1
1230    --pcie-generic-initiator port=sw1-downstream-0,node=1
1231
1232    # Also works for a root port name
1233    --pcie-generic-initiator port=rp0,node=2
1234
1235Syntax: port=<port_name>,node=<node>
1236"#)]
1237    #[clap(
1238        long = "pcie-generic-initiator",
1239        value_name = "port=<name>,node=<node>",
1240        conflicts_with("pcat")
1241    )]
1242    pub pcie_generic_initiator: Vec<PcieGenericInitiatorCli>,
1243
1244    /// Attach a PCIe remote device to a downstream port
1245    #[clap(long_help = r#"
1246Attach PCIe devices to root ports or downstream switch ports
1247which are implemented in a simulator running in a remote process.
1248
1249Examples:
1250    # Attach to root port rc0rp0 with default socket
1251    --pcie-remote rc0rp0
1252
1253    # Attach with custom socket address
1254    --pcie-remote rc0rp0,socket=0.0.0.0:48914
1255
1256    # Specify HU and controller identifiers
1257    --pcie-remote rc0rp0,hu=1,controller=0
1258
1259    # Multiple devices on different ports
1260    --pcie-remote rc0rp0,socket=0.0.0.0:48914
1261    --pcie-remote rc0rp1,socket=0.0.0.0:48915
1262
1263Syntax: <port_name>[,opt=arg,...]
1264
1265Options:
1266    `socket=<address>`              TCP socket (default: localhost:48914)
1267    `hu=<value>`                    Hardware unit identifier (default: 0)
1268    `controller=<value>`            Controller identifier (default: 0)
1269"#)]
1270    #[clap(long, conflicts_with("pcat"))]
1271    pub pcie_remote: Vec<PcieRemoteCli>,
1272
1273    /// Assign a host PCI device to the guest via VFIO (Linux only)
1274    #[clap(long_help = r#"
1275Assign a host PCI device to the guest via Linux VFIO.
1276
1277The device must be bound to vfio-pci on the host before starting the VM.
1278
1279Examples:
1280    --vfio host=0000:01:00.0,port=rp0
1281    --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1282
1283Keys:
1284    host=<pci_bdf>    (required) PCI address on the host
1285    port=<name>       (required) Root port or downstream switch port name
1286    iommu=<id>        (optional) Reference to an --iommu object. When present,
1287                      uses VFIO cdev + iommufd instead of the legacy group path.
1288"#)]
1289    #[cfg(target_os = "linux")]
1290    #[clap(long, conflicts_with("pcat"))]
1291    pub vfio: Vec<VfioDeviceCli>,
1292
1293    /// Create an iommufd context for VFIO cdev device assignment
1294    #[clap(long_help = r#"
1295Declare an iommufd context. Opens /dev/iommu so it can be referenced by
1296--vfio devices via the iommu=<id> key. The associated IOAS is allocated
1297the first time a --vfio device referring to this id is opened.
1298
1299Requires Linux kernel >= 6.6 with iommufd support.
1300
1301Examples:
1302    --iommu id=iommu0 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1303
1304Syntax: id=<name>
1305"#)]
1306    #[cfg(target_os = "linux")]
1307    #[clap(long, conflicts_with("pcat"))]
1308    pub iommu: Vec<IommuCli>,
1309}
1310
1311impl Options {
1312    /// Returns the effective guest RAM size.
1313    pub fn memory_size(&self) -> u64 {
1314        self.memory.size.map(|m| m.0).unwrap_or(DEFAULT_MEMORY_SIZE)
1315    }
1316
1317    /// Returns whether guest RAM should be prefetched.
1318    pub fn prefetch_memory(&self) -> bool {
1319        self.memory.prefetch || self.deprecated_prefetch
1320    }
1321
1322    /// Returns whether guest RAM should use private anonymous backing.
1323    pub fn private_memory(&self) -> bool {
1324        self.memory.shared == Some(false) || self.deprecated_private_memory
1325    }
1326
1327    /// Returns whether guest RAM should be marked THP-eligible.
1328    pub fn transparent_hugepages(&self) -> bool {
1329        self.memory
1330            .transparent_hugepages
1331            .unwrap_or(!self.memory.hugepages)
1332            || self.deprecated_thp
1333    }
1334
1335    /// Returns the effective file backing path for guest RAM.
1336    pub fn memory_backing_file(&self) -> Option<&PathBuf> {
1337        self.memory
1338            .file
1339            .as_ref()
1340            .or(self.deprecated_memory_backing_file.as_ref())
1341    }
1342
1343    /// Validates combinations that span the new `--memory` parser and legacy aliases.
1344    ///
1345    /// Only checks that cannot be expressed elsewhere live here. Conflicts
1346    /// within a single `--memory` string are enforced by the parser, and
1347    /// semantic constraints (platform support, private-vs-shared, huge pages
1348    /// vs. legacy RAM, etc.) are enforced by the membacking builder at VM
1349    /// build time; those are not duplicated here.
1350    pub fn validate_memory_options(&self) -> anyhow::Result<()> {
1351        if self.memory.file.is_some() && self.deprecated_memory_backing_file.is_some() {
1352            anyhow::bail!("--memory file=... conflicts with --memory-backing-file");
1353        }
1354        if self.memory.file.is_some() && self.restore_snapshot.is_some() {
1355            anyhow::bail!("--memory file=... conflicts with --restore-snapshot");
1356        }
1357        if self.memory.shared == Some(true) && self.deprecated_private_memory {
1358            anyhow::bail!("--memory shared=on conflicts with --private-memory");
1359        }
1360        Ok(())
1361    }
1362}
1363
1364#[derive(Clone, Debug, PartialEq)]
1365pub struct FsArgs {
1366    pub tag: String,
1367    pub path: String,
1368    pub pcie_port: Option<String>,
1369}
1370
1371impl FromStr for FsArgs {
1372    type Err = anyhow::Error;
1373
1374    fn from_str(s: &str) -> Result<Self, Self::Err> {
1375        let (pcie_port, s) = parse_pcie_port_prefix(s);
1376        let mut s = s.split(',');
1377        let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
1378            anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
1379        };
1380        Ok(Self {
1381            tag: tag.to_owned(),
1382            path: path.to_owned(),
1383            pcie_port,
1384        })
1385    }
1386}
1387
1388#[derive(Clone, Debug, PartialEq)]
1389pub struct FsArgsWithOptions {
1390    /// The file system tag.
1391    pub tag: String,
1392    /// The root path.
1393    pub path: String,
1394    /// The extra options, joined with ';'.
1395    pub options: String,
1396    /// Optional PCIe port name.
1397    pub pcie_port: Option<String>,
1398}
1399
1400impl FromStr for FsArgsWithOptions {
1401    type Err = anyhow::Error;
1402
1403    fn from_str(s: &str) -> Result<Self, Self::Err> {
1404        let (pcie_port, s) = parse_pcie_port_prefix(s);
1405        let mut s = s.split(',');
1406        let (Some(tag), Some(path)) = (s.next(), s.next()) else {
1407            anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
1408        };
1409        let options = s.collect::<Vec<_>>().join(";");
1410        Ok(Self {
1411            tag: tag.to_owned(),
1412            path: path.to_owned(),
1413            options,
1414            pcie_port,
1415        })
1416    }
1417}
1418
1419/// What the VMM does on a guest power event (reset, power-off/hibernate,
1420/// triple-fault, or watchdog timeout). Parsed from `reset`, `halt`, `exit`, or
1421/// `exit:<code>`; a bare `exit` uses status 0.
1422#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1423pub enum GuestPowerAction {
1424    /// Restart the guest.
1425    Reset,
1426    /// Stop the VM but keep the VMM process, so it can be inspected or
1427    /// restarted from the REPL.
1428    Halt,
1429    /// Exit the VMM process with this status code.
1430    Exit(u8),
1431}
1432
1433/// Parse a [`GuestPowerAction`] from `reset`, `halt`, `exit`, or `exit:<code>`.
1434/// A bare `exit` exits with status 0; `exit:<code>` exits with `<code>` (0-255).
1435fn parse_guest_power_action(s: &str) -> Result<GuestPowerAction, String> {
1436    match s {
1437        "reset" => Ok(GuestPowerAction::Reset),
1438        "halt" => Ok(GuestPowerAction::Halt),
1439        "exit" => Ok(GuestPowerAction::Exit(0)),
1440        _ => match s.strip_prefix("exit:") {
1441            Some(code) => code
1442                .parse::<u8>()
1443                .map(GuestPowerAction::Exit)
1444                .map_err(|err| format!("invalid exit code '{code}' (expected 0-255): {err}")),
1445            None => Err(format!(
1446                "expected reset, halt, exit, or exit:<code>, got '{s}'"
1447            )),
1448        },
1449    }
1450}
1451
1452#[derive(Copy, Clone, clap::ValueEnum)]
1453pub enum VirtioBusCli {
1454    Auto,
1455    Mmio,
1456    Pci,
1457    Vpci,
1458}
1459
1460#[cfg(target_os = "linux")]
1461fn parse_vhost_vsock_cid(value: &str) -> Result<u32, String> {
1462    let cid = value
1463        .parse::<u32>()
1464        .map_err(|error| format!("invalid CID '{value}': {error}"))?;
1465    if !(3..u32::MAX).contains(&cid) {
1466        return Err(format!("CID must be between 3 and {}", u32::MAX - 1));
1467    }
1468    Ok(cid)
1469}
1470
1471/// Parse an optional `pcie_port=<name>:` prefix from a CLI argument string.
1472///
1473/// Returns `(Some(port_name), rest)` if the prefix is present, or
1474/// `(None, original)` if not.
1475fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
1476    if let Some(rest) = s.strip_prefix("pcie_port=") {
1477        if let Some((port, rest)) = rest.split_once(':') {
1478            if !port.is_empty() {
1479                return (Some(port.to_string()), rest);
1480            }
1481        }
1482    }
1483    (None, s)
1484}
1485
1486#[derive(Clone, Debug, PartialEq)]
1487pub struct VirtioPmemArgs {
1488    pub path: String,
1489    pub pcie_port: Option<String>,
1490}
1491
1492impl FromStr for VirtioPmemArgs {
1493    type Err = anyhow::Error;
1494
1495    fn from_str(s: &str) -> Result<Self, Self::Err> {
1496        let (pcie_port, s) = parse_pcie_port_prefix(s);
1497        if s.is_empty() {
1498            anyhow::bail!("expected [pcie_port=<port>:]<path>");
1499        }
1500        Ok(Self {
1501            path: s.to_owned(),
1502            pcie_port,
1503        })
1504    }
1505}
1506
1507#[derive(clap::ValueEnum, Clone, Copy)]
1508pub enum SecureBootTemplateCli {
1509    Windows,
1510    UefiCa,
1511}
1512
1513fn parse_memory(s: &str) -> anyhow::Result<u64> {
1514    if s == "VMGS_DEFAULT" {
1515        Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
1516    } else {
1517        || -> Option<u64> {
1518            let mut b = s.as_bytes();
1519            if s.ends_with('B') {
1520                b = &b[..b.len() - 1]
1521            }
1522            if b.is_empty() {
1523                return None;
1524            }
1525            let multi = match b[b.len() - 1] as char {
1526                'T' => Some(1024 * 1024 * 1024 * 1024),
1527                'G' => Some(1024 * 1024 * 1024),
1528                'M' => Some(1024 * 1024),
1529                'K' => Some(1024),
1530                _ => None,
1531            };
1532            if multi.is_some() {
1533                b = &b[..b.len() - 1]
1534            }
1535            let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
1536            n.checked_mul(multi.unwrap_or(1))
1537        }()
1538        .with_context(|| format!("invalid memory size '{0}'", s))
1539    }
1540}
1541
1542/// Parses an address, which must be a `0x`-prefixed hexadecimal value.
1543fn parse_address(s: &str) -> anyhow::Result<u64> {
1544    let hex = s
1545        .strip_prefix("0x")
1546        .or_else(|| s.strip_prefix("0X"))
1547        .with_context(|| format!("invalid address '{s}', expected a 0x-prefixed hex value"))?;
1548    u64::from_str_radix(hex, 16).with_context(|| format!("invalid address '{s}'"))
1549}
1550
1551fn parse_acs_capability_mask(value: &str) -> anyhow::Result<u16> {
1552    if let Some(hex) = value
1553        .strip_prefix("0x")
1554        .or_else(|| value.strip_prefix("0X"))
1555    {
1556        u16::from_str_radix(hex, 16).context("invalid ACS capability mask")
1557    } else {
1558        value.parse::<u16>().context("invalid ACS capability mask")
1559    }
1560}
1561
1562fn parse_memory_config(s: &str) -> anyhow::Result<MemoryCli> {
1563    // Bare shortcut: `--memory 64G` sets only the size.
1564    let memory = if !s.contains('=') && !s.contains(',') {
1565        MemoryCli {
1566            size: Some(s.parse::<vmm_cli::MemorySize>()?),
1567            ..Default::default()
1568        }
1569    } else {
1570        s.parse::<MemoryCli>()?
1571    };
1572    memory.validate()?;
1573    Ok(memory)
1574}
1575
1576fn parse_numa_node(s: &str) -> anyhow::Result<NumaNodeCli> {
1577    let node: NumaNodeCli = s.parse()?;
1578    anyhow::ensure!(
1579        node.memory.size.is_some(),
1580        "numa node requires 'size' option"
1581    );
1582    anyhow::ensure!(
1583        node.memory.file.is_none(),
1584        "'file' is not supported in --numa"
1585    );
1586    node.memory.validate()?;
1587    Ok(node)
1588}
1589
1590fn parse_numa_distance(s: &str) -> anyhow::Result<NumaDistanceCli> {
1591    let parts: Vec<&str> = s.split(':').collect();
1592    anyhow::ensure!(
1593        parts.len() == 3,
1594        "expected SRC:DST:DISTANCE format, got '{s}'"
1595    );
1596    let src = parts[0].parse::<u32>().context("invalid source node")?;
1597    let dst = parts[1]
1598        .parse::<u32>()
1599        .context("invalid destination node")?;
1600    let distance = parts[2].parse::<u8>().context("invalid distance")?;
1601    anyhow::ensure!(
1602        distance >= 10,
1603        "distance must be >= 10 (10 = local), got {distance}"
1604    );
1605    Ok(NumaDistanceCli { src, dst, distance })
1606}
1607
1608/// Parse a number from a string that could be prefixed with 0x to indicate hex.
1609fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
1610    match s.strip_prefix("0x") {
1611        Some(rest) => u64::from_str_radix(rest, 16),
1612        None => s.parse::<u64>(),
1613    }
1614}
1615
1616#[derive(Clone, Debug, PartialEq)]
1617pub enum DiskCliKind {
1618    // mem:<len>
1619    Memory(u64),
1620    // memdiff:<kind>
1621    MemoryDiff(Box<DiskCliKind>),
1622    // sql:<path>[;create=<len>]
1623    Sqlite {
1624        path: PathBuf,
1625        create_with_len: Option<u64>,
1626    },
1627    // sqldiff:<path>[;create]:<kind>
1628    SqliteDiff {
1629        path: PathBuf,
1630        create: bool,
1631        disk: Box<DiskCliKind>,
1632    },
1633    // autocache:[key]:<kind>
1634    AutoCacheSqlite {
1635        cache_path: String,
1636        key: Option<String>,
1637        disk: Box<DiskCliKind>,
1638    },
1639    // prwrap:<kind>
1640    PersistentReservationsWrapper(Box<DiskCliKind>),
1641    // file:<path>[;direct][;create=<len>]
1642    File {
1643        path: PathBuf,
1644        create_with_len: Option<u64>,
1645        direct: bool,
1646    },
1647    // blob:<type>:<url>
1648    Blob {
1649        kind: BlobKind,
1650        url: String,
1651    },
1652    // crypt:<cipher>:<key_file>:<kind>
1653    Crypt {
1654        cipher: DiskCipher,
1655        key_file: PathBuf,
1656        disk: Box<DiskCliKind>,
1657    },
1658    // delay:<delay_ms>:<kind>
1659    DelayDiskWrapper {
1660        delay_ms: u64,
1661        disk: Box<DiskCliKind>,
1662    },
1663}
1664
1665#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
1666pub enum DiskCipher {
1667    #[clap(name = "xts-aes-256")]
1668    XtsAes256,
1669}
1670
1671#[derive(Copy, Clone, Debug, PartialEq)]
1672pub enum BlobKind {
1673    Flat,
1674    Vhd1,
1675}
1676
1677struct FileOpts {
1678    path: PathBuf,
1679    create_with_len: Option<u64>,
1680    direct: bool,
1681}
1682
1683fn parse_file_opts(arg: &str) -> anyhow::Result<FileOpts> {
1684    let mut path = arg;
1685    let mut create_with_len = None;
1686    let mut direct = false;
1687
1688    // Parse semicolon-delimited options after the path.
1689    if let Some((p, rest)) = arg.split_once(';') {
1690        path = p;
1691        for opt in rest.split(';') {
1692            if let Some(len) = opt.strip_prefix("create=") {
1693                create_with_len = Some(parse_memory(len)?);
1694            } else if opt == "direct" {
1695                direct = true;
1696            } else {
1697                anyhow::bail!("invalid file option '{opt}', expected 'create=<len>' or 'direct'");
1698            }
1699        }
1700    }
1701
1702    Ok(FileOpts {
1703        path: path.into(),
1704        create_with_len,
1705        direct,
1706    })
1707}
1708
1709impl DiskCliKind {
1710    /// Parse an `autocache:[key]:<kind>` disk spec, given the cache path
1711    /// (normally read from `OPENVMM_AUTO_CACHE_PATH`).
1712    fn parse_autocache(
1713        arg: &str,
1714        cache_path: Result<String, std::env::VarError>,
1715    ) -> anyhow::Result<Self> {
1716        let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
1717        let cache_path = cache_path.context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
1718        Ok(DiskCliKind::AutoCacheSqlite {
1719            cache_path,
1720            key: (!key.is_empty()).then(|| key.to_string()),
1721            disk: Box::new(kind.parse()?),
1722        })
1723    }
1724}
1725
1726impl FromStr for DiskCliKind {
1727    type Err = anyhow::Error;
1728
1729    fn from_str(s: &str) -> anyhow::Result<Self> {
1730        let disk = match s.split_once(':') {
1731            // convenience support for passing bare paths as file disks
1732            None => {
1733                let FileOpts {
1734                    path,
1735                    create_with_len,
1736                    direct,
1737                } = parse_file_opts(s)?;
1738                DiskCliKind::File {
1739                    path,
1740                    create_with_len,
1741                    direct,
1742                }
1743            }
1744            Some((kind, arg)) => match kind {
1745                "mem" => DiskCliKind::Memory(parse_memory(arg)?),
1746                "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
1747                "sql" => {
1748                    let FileOpts {
1749                        path,
1750                        create_with_len,
1751                        direct,
1752                    } = parse_file_opts(arg)?;
1753                    if direct {
1754                        anyhow::bail!("'direct' is not supported for 'sql' disks");
1755                    }
1756                    DiskCliKind::Sqlite {
1757                        path,
1758                        create_with_len,
1759                    }
1760                }
1761                "sqldiff" => {
1762                    let (path_and_opts, kind) =
1763                        arg.split_once(':').context("expected path[;opts]:kind")?;
1764                    let disk = Box::new(kind.parse()?);
1765                    match path_and_opts.split_once(';') {
1766                        Some((path, create)) => {
1767                            if create != "create" {
1768                                anyhow::bail!("invalid syntax after ';', expected 'create'")
1769                            }
1770                            DiskCliKind::SqliteDiff {
1771                                path: path.into(),
1772                                create: true,
1773                                disk,
1774                            }
1775                        }
1776                        None => DiskCliKind::SqliteDiff {
1777                            path: path_and_opts.into(),
1778                            create: false,
1779                            disk,
1780                        },
1781                    }
1782                }
1783                "autocache" => {
1784                    Self::parse_autocache(arg, std::env::var("OPENVMM_AUTO_CACHE_PATH"))?
1785                }
1786                "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
1787                "file" => {
1788                    let FileOpts {
1789                        path,
1790                        create_with_len,
1791                        direct,
1792                    } = parse_file_opts(arg)?;
1793                    DiskCliKind::File {
1794                        path,
1795                        create_with_len,
1796                        direct,
1797                    }
1798                }
1799                "blob" => {
1800                    let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
1801                    let blob_kind = match blob_kind {
1802                        "flat" => BlobKind::Flat,
1803                        "vhd1" => BlobKind::Vhd1,
1804                        _ => anyhow::bail!("unknown blob kind {blob_kind}"),
1805                    };
1806                    DiskCliKind::Blob {
1807                        kind: blob_kind,
1808                        url: url.to_string(),
1809                    }
1810                }
1811                "crypt" => {
1812                    let (cipher, (key, kind)) = arg
1813                        .split_once(':')
1814                        .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
1815                        .context("expected cipher:key_file:kind")?;
1816                    DiskCliKind::Crypt {
1817                        cipher: ValueEnum::from_str(cipher, false)
1818                            .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
1819                        key_file: PathBuf::from(key),
1820                        disk: Box::new(kind.parse()?),
1821                    }
1822                }
1823                kind => {
1824                    // here's a fun edge case: what if the user passes `--disk d:\path\to\disk.img`?
1825                    //
1826                    // in this case, we actually want to treat that leading `d:` as part of the
1827                    // path, rather than as a disk with `kind == 'd'`
1828                    let FileOpts {
1829                        path,
1830                        create_with_len,
1831                        direct,
1832                    } = parse_file_opts(s)?;
1833                    if path.has_root() {
1834                        DiskCliKind::File {
1835                            path,
1836                            create_with_len,
1837                            direct,
1838                        }
1839                    } else {
1840                        anyhow::bail!("invalid disk kind {kind}");
1841                    }
1842                }
1843            },
1844        };
1845        Ok(disk)
1846    }
1847}
1848
1849/// Wire transport selection for `--rpc`.
1850#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1851pub enum RpcTransportCli {
1852    /// Auto-detect ttrpc vs. gRPC per connection, based on the first byte of
1853    /// the stream.
1854    #[default]
1855    Auto,
1856    /// Accept ttrpc clients only.
1857    Ttrpc,
1858    /// Accept gRPC clients only.
1859    Grpc,
1860}
1861
1862/// RPC server configuration parsed from `--rpc`.
1863#[derive(Clone, Debug, PartialEq, Eq)]
1864pub struct RpcCli {
1865    /// Unix socket path to listen on.
1866    pub path: PathBuf,
1867    /// Wire transport to accept.
1868    pub transport: RpcTransportCli,
1869}
1870
1871impl FromStr for RpcCli {
1872    type Err = anyhow::Error;
1873
1874    fn from_str(s: &str) -> anyhow::Result<Self> {
1875        let mut path = None;
1876        let mut transport = None;
1877        for part in s.split(',') {
1878            let (key, value) = part
1879                .split_once('=')
1880                .with_context(|| format!("invalid rpc option '{part}', expected key=value"))?;
1881            match key {
1882                "path" => {
1883                    anyhow::ensure!(path.is_none(), "duplicate option 'path'");
1884                    anyhow::ensure!(!value.is_empty(), "'path' requires a value");
1885                    path = Some(PathBuf::from(value));
1886                }
1887                "transport" => {
1888                    anyhow::ensure!(transport.is_none(), "duplicate option 'transport'");
1889                    transport = Some(match value {
1890                        "auto" => RpcTransportCli::Auto,
1891                        "ttrpc" => RpcTransportCli::Ttrpc,
1892                        "grpc" => RpcTransportCli::Grpc,
1893                        _ => anyhow::bail!(
1894                            "invalid transport '{value}', expected auto, ttrpc, or grpc"
1895                        ),
1896                    });
1897                }
1898                _ => anyhow::bail!("unknown rpc option '{key}'"),
1899            }
1900        }
1901
1902        Ok(RpcCli {
1903            path: path.context("'path' is required")?,
1904            transport: transport.unwrap_or_default(),
1905        })
1906    }
1907}
1908
1909#[derive(Clone)]
1910pub struct VmgsCli {
1911    pub kind: DiskCliKind,
1912    pub provision: ProvisionVmgs,
1913}
1914
1915#[derive(Copy, Clone)]
1916pub enum ProvisionVmgs {
1917    OnEmpty,
1918    OnFailure,
1919    True,
1920}
1921
1922impl FromStr for VmgsCli {
1923    type Err = anyhow::Error;
1924
1925    fn from_str(s: &str) -> anyhow::Result<Self> {
1926        let (kind, opt) = s
1927            .split_once(',')
1928            .map(|(k, o)| (k, Some(o)))
1929            .unwrap_or((s, None));
1930        let kind = kind.parse()?;
1931
1932        let provision = match opt {
1933            None => ProvisionVmgs::OnEmpty,
1934            Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
1935            Some("fmt") => ProvisionVmgs::True,
1936            Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
1937        };
1938
1939        Ok(VmgsCli { kind, provision })
1940    }
1941}
1942
1943/// VNC server configuration options.
1944#[derive(clap::Args)]
1945pub struct VncCli {
1946    /// Listen for VNC connections. Implied by --gfx.
1947    #[clap(long)]
1948    pub vnc: bool,
1949
1950    /// VNC port number
1951    #[clap(long, value_name = "PORT", default_value = "5900")]
1952    pub vnc_port: u16,
1953
1954    /// VNC listen address (use 0.0.0.0 for all IPv4, :: for dual-stack IPv4+IPv6).
1955    /// Accepts a bare IP address (combined with --vnc-port) or a full socket
1956    /// address like [::1]:5900 (overrides --vnc-port).
1957    #[clap(long, value_name = "ADDRESS", default_value = "127.0.0.1")]
1958    pub vnc_listen: String,
1959
1960    /// Maximum concurrent VNC clients (~8MB memory per client for framebuffer buffers)
1961    #[clap(long, value_name = "COUNT", default_value = "16")]
1962    pub vnc_max_clients: usize,
1963
1964    /// When the client limit is reached, disconnect the oldest client
1965    /// instead of rejecting the new connection
1966    #[clap(long)]
1967    pub vnc_evict_oldest: bool,
1968}
1969
1970// <kind>[,ro]
1971#[derive(Clone)]
1972pub struct DiskCli {
1973    pub vtl: DeviceVtl,
1974    pub kind: DiskCliKind,
1975    pub read_only: bool,
1976    pub is_dvd: bool,
1977    pub underhill: Option<UnderhillDiskSource>,
1978    pub pcie_port: Option<String>,
1979    pub controller: Option<String>,
1980    pub nsid: Option<u32>,
1981    pub lun: Option<u8>,
1982    pub relay: Option<(String, Option<u32>)>,
1983}
1984
1985#[derive(Copy, Clone)]
1986pub enum UnderhillDiskSource {
1987    Scsi,
1988    Nvme,
1989}
1990
1991/// A `relay=<name>[:<location>]` target for an OpenHCL-managed controller.
1992struct RelayTarget {
1993    name: String,
1994    location: Option<u32>,
1995}
1996
1997impl FromStr for RelayTarget {
1998    type Err = anyhow::Error;
1999
2000    fn from_str(s: &str) -> anyhow::Result<Self> {
2001        Ok(if let Some((name, loc)) = s.split_once(':') {
2002            RelayTarget {
2003                name: name.to_string(),
2004                location: Some(loc.parse::<u32>().context("invalid relay location")?),
2005            }
2006        } else {
2007            RelayTarget {
2008                name: s.to_string(),
2009                location: None,
2010            }
2011        })
2012    }
2013}
2014
2015/// Raw `--disk`/`--nvme`/`--virtio-blk` options, resolved and validated into a
2016/// [`DiskCli`] by its `FromStr`.
2017#[derive(vmm_cli::KeyValueArgs)]
2018struct DiskArgs {
2019    #[kv(positional)]
2020    kind: DiskCliKind,
2021    #[kv(flag)]
2022    ro: bool,
2023    #[kv(flag)]
2024    dvd: bool,
2025    #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2026    vtl: DeviceVtl,
2027    #[kv(flag)]
2028    uh: bool,
2029    #[kv(flag, key = "uh-nvme")]
2030    uh_nvme: bool,
2031    pcie_port: Option<String>,
2032    #[kv(key = "on")]
2033    controller: Option<String>,
2034    nsid: Option<u32>,
2035    lun: Option<u8>,
2036    relay: Option<RelayTarget>,
2037}
2038
2039impl FromStr for DiskCli {
2040    type Err = anyhow::Error;
2041
2042    fn from_str(s: &str) -> anyhow::Result<Self> {
2043        let args: DiskArgs = s.parse()?;
2044
2045        let underhill = match (args.uh, args.uh_nvme) {
2046            (false, false) => None,
2047            (true, false) => Some(UnderhillDiskSource::Scsi),
2048            (false, true) => Some(UnderhillDiskSource::Nvme),
2049            (true, true) => anyhow::bail!("`uh` and `uh-nvme` are mutually exclusive"),
2050        };
2051        let read_only = args.ro || args.dvd;
2052        let is_dvd = args.dvd;
2053        let vtl = args.vtl;
2054        let pcie_port = args.pcie_port;
2055        let controller = args.controller;
2056        let nsid = args.nsid;
2057        let lun = args.lun;
2058        let relay = args.relay.map(|r| (r.name, r.location));
2059
2060        if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
2061            anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
2062        }
2063
2064        if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
2065            anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
2066        }
2067
2068        if controller.is_some() && pcie_port.is_some() {
2069            anyhow::bail!("`on` is incompatible with `pcie_port`");
2070        }
2071
2072        if controller.is_some() && vtl != DeviceVtl::Vtl0 {
2073            anyhow::bail!(
2074                "`vtl2` is incompatible with `on`; the controller's VTL determines placement"
2075            );
2076        }
2077
2078        if controller.is_some() && underhill.is_some() {
2079            anyhow::bail!("`on` is incompatible with `uh` and `uh-nvme`; use `relay` instead");
2080        }
2081
2082        if nsid.is_some() && controller.is_none() {
2083            anyhow::bail!("`nsid` requires `on`");
2084        }
2085
2086        if lun.is_some() && controller.is_none() {
2087            anyhow::bail!("`lun` requires `on`");
2088        }
2089
2090        if nsid.is_some() && lun.is_some() {
2091            anyhow::bail!("`nsid` and `lun` are mutually exclusive");
2092        }
2093
2094        if relay.is_some() && controller.is_none() {
2095            anyhow::bail!("`relay` requires `on`");
2096        }
2097
2098        if relay.is_some() && underhill.is_some() {
2099            anyhow::bail!("`relay` is incompatible with `uh` and `uh-nvme`");
2100        }
2101
2102        Ok(DiskCli {
2103            vtl,
2104            kind: args.kind,
2105            read_only,
2106            is_dvd,
2107            underhill,
2108            pcie_port,
2109            controller,
2110            nsid,
2111            lun,
2112            relay,
2113        })
2114    }
2115}
2116
2117/// The transport for a named NVMe controller.
2118#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueGroup)]
2119pub enum NvmeControllerTransport {
2120    /// Present via PCIe on the specified root port.
2121    #[kv(key = "pcie_port")]
2122    Pcie(String),
2123    /// Present via VPCI with an optional instance GUID.
2124    #[kv(key = "vpci")]
2125    Vpci(Option<Guid>),
2126}
2127
2128/// CLI arguments for a named NVMe controller.
2129#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2130pub struct NvmeControllerCli {
2131    /// Controller name, referenced by `--disk on=<name>`.
2132    pub id: String,
2133    /// Transport configuration.
2134    #[kv(flatten)]
2135    pub transport: NvmeControllerTransport,
2136    /// VTL assignment (default VTL0).
2137    #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2138    pub vtl: DeviceVtl,
2139}
2140
2141/// CLI arguments for a named VMBus SCSI controller.
2142#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2143pub struct ScsiControllerCli {
2144    /// Controller name, referenced by `--disk on=<name>`.
2145    pub id: String,
2146    /// Number of sub-channels.
2147    #[kv(default)]
2148    pub sub_channels: u16,
2149    /// VTL assignment (default VTL0).
2150    #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2151    pub vtl: DeviceVtl,
2152}
2153
2154/// Protocol type for an OpenHCL-managed controller.
2155#[derive(Copy, Clone, Debug, PartialEq)]
2156pub enum OpenhclControllerType {
2157    Scsi,
2158    Nvme,
2159}
2160
2161/// CLI arguments for an OpenHCL-managed storage controller (relay target).
2162#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2163pub struct OpenhclControllerCli {
2164    /// Controller name, referenced by `--disk ... relay=<name>`.
2165    pub id: String,
2166    /// Controller protocol.
2167    #[kv(key = "type")]
2168    pub controller_type: OpenhclControllerType,
2169    /// Instance GUID (auto-derived from name if omitted).
2170    pub guid: Option<Guid>,
2171}
2172
2173impl FromStr for OpenhclControllerType {
2174    type Err = anyhow::Error;
2175
2176    fn from_str(s: &str) -> anyhow::Result<Self> {
2177        Ok(match s {
2178            "scsi" => OpenhclControllerType::Scsi,
2179            "nvme" => OpenhclControllerType::Nvme,
2180            other => anyhow::bail!("unknown controller type: '{other}'"),
2181        })
2182    }
2183}
2184
2185/// CLI arguments for a CXL Type-3 test endpoint.
2186#[derive(Clone, Debug, PartialEq)]
2187pub struct CxlTestDeviceCli {
2188    /// Size of HDM memory the test device should expose and back.
2189    pub hdm_size: u64,
2190    /// PCIe root port name where the device is attached.
2191    pub pcie_port: String,
2192}
2193
2194impl FromStr for CxlTestDeviceCli {
2195    type Err = anyhow::Error;
2196
2197    fn from_str(s: &str) -> anyhow::Result<Self> {
2198        let mut opts = s.split(',');
2199        let first = opts.next().context("expected CXL test device config")?;
2200        let (kind, arg) = first
2201            .split_once(':')
2202            .context("expected CXL test syntax: mem:<len>")?;
2203
2204        if kind != "mem" {
2205            anyhow::bail!("unsupported CXL test backing kind '{kind}', expected 'mem'");
2206        }
2207
2208        let hdm_size = parse_memory(arg).context("failed to parse CXL test HDM size")?;
2209        let mut pcie_port = None;
2210
2211        for opt in opts {
2212            let mut kv = opt.split('=');
2213            let key = kv.next().unwrap_or_default();
2214            match key {
2215                "pcie_port" => {
2216                    let val = kv.next();
2217                    if val.is_none_or(|v| v.is_empty()) {
2218                        anyhow::bail!("`pcie_port` requires a port name");
2219                    }
2220                    pcie_port = Some(val.unwrap().to_string());
2221                }
2222                _ => anyhow::bail!("unknown option: '{opt}'"),
2223            }
2224        }
2225
2226        let Some(pcie_port) = pcie_port else {
2227            anyhow::bail!("`pcie_port=<name>` is required for `--cxl-test`");
2228        };
2229
2230        Ok(Self {
2231            hdm_size,
2232            pcie_port,
2233        })
2234    }
2235}
2236
2237// <kind>[,ro,s]
2238#[derive(Clone)]
2239pub struct IdeDiskCli {
2240    pub kind: DiskCliKind,
2241    pub read_only: bool,
2242    pub channel: Option<u8>,
2243    pub device: Option<u8>,
2244    pub is_dvd: bool,
2245}
2246
2247impl FromStr for IdeDiskCli {
2248    type Err = anyhow::Error;
2249
2250    fn from_str(s: &str) -> anyhow::Result<Self> {
2251        let mut opts = s.split(',');
2252        let kind = opts.next().unwrap().parse()?;
2253
2254        let mut read_only = false;
2255        let mut channel = None;
2256        let mut device = None;
2257        let mut is_dvd = false;
2258        for opt in opts {
2259            let mut s = opt.split('=');
2260            let opt = s.next().unwrap();
2261            match opt {
2262                "ro" => read_only = true,
2263                "p" => channel = Some(0),
2264                "s" => channel = Some(1),
2265                "0" => device = Some(0),
2266                "1" => device = Some(1),
2267                "dvd" => {
2268                    is_dvd = true;
2269                    read_only = true;
2270                }
2271                _ => anyhow::bail!("unknown option: '{opt}'"),
2272            }
2273        }
2274
2275        Ok(IdeDiskCli {
2276            kind,
2277            read_only,
2278            channel,
2279            device,
2280            is_dvd,
2281        })
2282    }
2283}
2284
2285// <kind>[,ro]
2286#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
2287pub struct FloppyDiskCli {
2288    #[kv(positional)]
2289    pub kind: DiskCliKind,
2290    #[kv(flag, key = "ro")]
2291    pub read_only: bool,
2292}
2293
2294#[derive(Clone)]
2295pub struct DebugconSerialConfigCli {
2296    pub port: u16,
2297    pub serial: SerialConfigCli,
2298}
2299
2300impl FromStr for DebugconSerialConfigCli {
2301    type Err = String;
2302
2303    fn from_str(s: &str) -> Result<Self, Self::Err> {
2304        let Some((port, serial)) = s.split_once(',') else {
2305            return Err("invalid format (missing comma between port and serial)".into());
2306        };
2307
2308        let port: u16 = parse_number(port)
2309            .map_err(|_| "could not parse port".to_owned())?
2310            .try_into()
2311            .map_err(|_| "port must be 16-bit")?;
2312        let serial: SerialConfigCli = serial.parse()?;
2313
2314        Ok(Self { port, serial })
2315    }
2316}
2317
2318/// A COM port binding, optionally prefixed with `debugger-mode:` to run the
2319/// port in debugger mode for WinDbg / KD-over-serial.
2320#[derive(Clone, Debug, PartialEq)]
2321pub struct ComSerialConfigCli {
2322    /// Whether this COM port runs in debugger mode (for WinDbg / KD-over-serial).
2323    pub debugger_mode: bool,
2324    /// The serial backend for this COM port.
2325    pub backend: SerialConfigCli,
2326}
2327
2328impl FromStr for ComSerialConfigCli {
2329    type Err = String;
2330
2331    fn from_str(s: &str) -> Result<Self, Self::Err> {
2332        match s.strip_prefix("debugger-mode:") {
2333            Some(rest) => Ok(Self {
2334                debugger_mode: true,
2335                backend: rest.parse()?,
2336            }),
2337            None => Ok(Self {
2338                debugger_mode: false,
2339                backend: s.parse()?,
2340            }),
2341        }
2342    }
2343}
2344
2345/// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
2346#[derive(Clone, Debug, PartialEq)]
2347pub enum SerialConfigCli {
2348    None,
2349    Console,
2350    NewConsole(Option<PathBuf>, Option<String>),
2351    Stderr,
2352    Pipe(PathBuf),
2353    Tcp(SocketAddr),
2354    File(PathBuf),
2355}
2356
2357impl FromStr for SerialConfigCli {
2358    type Err = String;
2359
2360    fn from_str(s: &str) -> Result<Self, Self::Err> {
2361        let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
2362
2363        let first_key = match keyvalues.first() {
2364            Some(first_pair) => first_pair.0.as_str(),
2365            None => Err("invalid serial configuration: no values supplied")?,
2366        };
2367        let first_value = keyvalues.first().unwrap().1.as_ref();
2368
2369        let ret = match first_key {
2370            "none" => SerialConfigCli::None,
2371            "console" => SerialConfigCli::Console,
2372            "stderr" => SerialConfigCli::Stderr,
2373            "file" => match first_value {
2374                Some(path) => SerialConfigCli::File(path.into()),
2375                None => Err("invalid serial configuration: file requires a value")?,
2376            },
2377            "term" => {
2378                // If user supplies a name key, use it to title the window
2379                let window_name = keyvalues.iter().find(|(key, _)| key == "name");
2380                let window_name = match window_name {
2381                    Some((_, Some(name))) => Some(name.clone()),
2382                    _ => None,
2383                };
2384
2385                SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
2386            }
2387            "listen" => match first_value {
2388                Some(path) => {
2389                    if let Some(tcp) = path.strip_prefix("tcp:") {
2390                        let addr = tcp
2391                            .parse()
2392                            .map_err(|err| format!("invalid tcp address: {err}"))?;
2393                        SerialConfigCli::Tcp(addr)
2394                    } else {
2395                        SerialConfigCli::Pipe(path.into())
2396                    }
2397                }
2398                None => Err(
2399                    "invalid serial configuration: listen requires a value of tcp:addr or pipe",
2400                )?,
2401            },
2402            _ => {
2403                return Err(format!(
2404                    "invalid serial configuration: '{}' is not a known option",
2405                    first_key
2406                ));
2407            }
2408        };
2409
2410        Ok(ret)
2411    }
2412}
2413
2414impl SerialConfigCli {
2415    /// Parse a comma separated list of key=value options into a vector of
2416    /// key/value pairs.
2417    fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
2418        let mut ret = Vec::new();
2419
2420        // For each comma separated item in the supplied list
2421        for item in s.split(',') {
2422            // Split on the = for key and value
2423            // If no = is found, treat key as key and value as None
2424            let mut eqsplit = item.split('=');
2425            let key = eqsplit.next();
2426            let value = eqsplit.next();
2427
2428            if let Some(key) = key {
2429                ret.push((key.to_owned(), value.map(|x| x.to_owned())));
2430            } else {
2431                // An empty key is invalid
2432                return Err("invalid key=value pair in serial config".into());
2433            }
2434        }
2435        Ok(ret)
2436    }
2437}
2438
2439#[derive(Clone, Debug, PartialEq)]
2440pub enum EndpointConfigCli {
2441    None,
2442    Consomme {
2443        cidr: Option<String>,
2444        host_fwd: Vec<HostPortConfigCli>,
2445    },
2446    Dio {
2447        id: Option<String>,
2448    },
2449    Tap {
2450        name: String,
2451    },
2452}
2453
2454/// Parsed host port forwarding configuration from the CLI.
2455#[derive(Clone, Debug, PartialEq)]
2456pub struct HostPortConfigCli {
2457    pub protocol: HostPortProtocolCli,
2458    pub host_address: Option<std::net::IpAddr>,
2459    pub host_port: u16,
2460    pub guest_port: u16,
2461}
2462
2463/// Protocol for host port forwarding.
2464#[derive(Clone, Debug, PartialEq)]
2465pub enum HostPortProtocolCli {
2466    Tcp,
2467    Udp,
2468}
2469
2470fn parse_hostfwd(s: &str) -> Result<HostPortConfigCli, String> {
2471    // Format: protocol:[hostaddr]:hostport-[guestaddr]:guestport
2472    // Examples: "tcp::3389-:3389", "tcp:127.0.0.1:8080-:80", "tcp:[::1]:8080-:80"
2473    let (host_part, guest_part) = s.split_once('-').ok_or_else(|| {
2474        format!(
2475            "invalid hostfwd format '{s}', \
2476             expected 'proto:[hostaddr]:hostport-[guestaddr]:guestport'"
2477        )
2478    })?;
2479
2480    // Extract protocol from host part (first colon-delimited field)
2481    let (proto, host_addr_port) = host_part.split_once(':').ok_or_else(|| {
2482        format!("invalid hostfwd host part '{host_part}', expected 'proto:[hostaddr]:hostport'")
2483    })?;
2484    let protocol = match proto {
2485        "tcp" => HostPortProtocolCli::Tcp,
2486        "udp" => HostPortProtocolCli::Udp,
2487        other => {
2488            return Err(format!(
2489                "unknown hostfwd protocol '{other}', expected 'tcp' or 'udp'"
2490            ));
2491        }
2492    };
2493
2494    let (host_address, host_port) = parse_addr_port(host_addr_port)
2495        .map_err(|e| format!("invalid hostfwd host address/port: {e}"))?;
2496    let (_, guest_port) = parse_addr_port(guest_part)
2497        .map_err(|e| format!("invalid hostfwd guest address/port: {e}"))?;
2498
2499    Ok(HostPortConfigCli {
2500        protocol,
2501        host_address,
2502        host_port,
2503        guest_port,
2504    })
2505}
2506
2507/// Parse an address-port pair in one of these forms:
2508/// - `[ipv6addr]:port`
2509/// - `addr:port`
2510/// - `:port`  (empty address)
2511/// - `port`   (no address)
2512fn parse_addr_port(s: &str) -> Result<(Option<std::net::IpAddr>, u16), String> {
2513    if let Some(rest) = s.strip_prefix('[') {
2514        // Bracketed IPv6 address: [addr]:port
2515        let (addr, port) = rest
2516            .split_once("]:")
2517            .ok_or_else(|| format!("expected '[addr]:port', got '[{rest}'"))?;
2518        let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
2519        let addr: std::net::IpAddr = addr
2520            .parse()
2521            .map_err(|e| format!("invalid address '{addr}': {e}"))?;
2522        Ok((Some(addr), port))
2523    } else {
2524        match s.rsplit_once(':') {
2525            Some((addr, port)) => {
2526                let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
2527                let addr = if addr.is_empty() {
2528                    None
2529                } else {
2530                    let parsed: std::net::IpAddr = addr
2531                        .parse()
2532                        .map_err(|e| format!("invalid address '{addr}': {e}"))?;
2533                    Some(parsed)
2534                };
2535                Ok((addr, port))
2536            }
2537            None => {
2538                let port: u16 = s.parse().map_err(|_| format!("invalid port '{s}'"))?;
2539                Ok((None, port))
2540            }
2541        }
2542    }
2543}
2544
2545impl FromStr for EndpointConfigCli {
2546    type Err = String;
2547
2548    fn from_str(s: &str) -> Result<Self, Self::Err> {
2549        let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
2550            ["none"] => EndpointConfigCli::None,
2551            ["consomme", rest @ ..] => {
2552                let remaining = rest.join(":");
2553                let mut cidr = None;
2554                let mut host_fwd = Vec::new();
2555                for opt in remaining.split(',').filter(|s| !s.is_empty()) {
2556                    if let Some(fwd) = opt.strip_prefix("hostfwd=") {
2557                        host_fwd.push(parse_hostfwd(fwd)?);
2558                    } else if cidr.is_none() {
2559                        cidr = Some(opt.to_owned());
2560                    } else {
2561                        return Err(format!("unexpected consomme option '{opt}'"));
2562                    }
2563                }
2564                EndpointConfigCli::Consomme { cidr, host_fwd }
2565            }
2566            ["dio", s @ ..] => EndpointConfigCli::Dio {
2567                id: s.first().map(|s| (*s).to_owned()),
2568            },
2569            ["tap", name] => EndpointConfigCli::Tap {
2570                name: (*name).to_owned(),
2571            },
2572            _ => return Err("invalid network backend".into()),
2573        };
2574
2575        Ok(ret)
2576    }
2577}
2578
2579#[derive(Clone, Debug, PartialEq)]
2580pub struct NicConfigCli {
2581    pub vtl: DeviceVtl,
2582    pub endpoint: EndpointConfigCli,
2583    pub max_queues: Option<u16>,
2584    pub underhill: bool,
2585    pub pcie_port: Option<String>,
2586}
2587
2588impl FromStr for NicConfigCli {
2589    type Err = String;
2590
2591    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
2592        let mut vtl = DeviceVtl::Vtl0;
2593        let mut max_queues = None;
2594        let mut underhill = false;
2595        let mut pcie_port = None;
2596        while let Some((opt, rest)) = s.split_once(':') {
2597            if let Some((opt, val)) = opt.split_once('=') {
2598                match opt {
2599                    "queues" => {
2600                        max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
2601                    }
2602                    "pcie_port" => {
2603                        if val.is_empty() {
2604                            return Err("`pcie_port=` requires port name argument".into());
2605                        }
2606                        pcie_port = Some(val.to_string());
2607                    }
2608                    _ => break,
2609                }
2610            } else {
2611                match opt {
2612                    "vtl2" => {
2613                        vtl = DeviceVtl::Vtl2;
2614                    }
2615                    "uh" => underhill = true,
2616                    _ => break,
2617                }
2618            }
2619            s = rest;
2620        }
2621
2622        if underhill && vtl != DeviceVtl::Vtl0 {
2623            return Err("`uh` is incompatible with `vtl2`".into());
2624        }
2625
2626        if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
2627            return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
2628        }
2629
2630        let endpoint = s.parse()?;
2631        Ok(NicConfigCli {
2632            vtl,
2633            endpoint,
2634            max_queues,
2635            underhill,
2636            pcie_port,
2637        })
2638    }
2639}
2640
2641#[derive(Debug, Error)]
2642#[error("unknown VTL2 relocation type: {0}")]
2643pub struct UnknownVtl2RelocationType(String);
2644
2645fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
2646    match s {
2647        "disable" => Ok(Vtl2BaseAddressType::File),
2648        s if s.starts_with("auto=") => {
2649            let s = s.strip_prefix("auto=").unwrap_or_default();
2650            let size = if s == "filesize" {
2651                None
2652            } else {
2653                let size = parse_memory(s).map_err(|e| {
2654                    UnknownVtl2RelocationType(format!(
2655                        "unable to parse memory size from {} for 'auto=' type, {e}",
2656                        e
2657                    ))
2658                })?;
2659                Some(size)
2660            };
2661            Ok(Vtl2BaseAddressType::MemoryLayout { size })
2662        }
2663        s if s.starts_with("absolute=") => {
2664            let s = s.strip_prefix("absolute=");
2665            let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
2666                UnknownVtl2RelocationType(format!(
2667                    "unable to parse number from {} for 'absolute=' type",
2668                    e
2669                ))
2670            })?;
2671            Ok(Vtl2BaseAddressType::Absolute(addr))
2672        }
2673        s if s.starts_with("vtl2=") => {
2674            let s = s.strip_prefix("vtl2=").unwrap_or_default();
2675            let size = if s == "filesize" {
2676                None
2677            } else {
2678                let size = parse_memory(s).map_err(|e| {
2679                    UnknownVtl2RelocationType(format!(
2680                        "unable to parse memory size from {} for 'vtl2=' type, {e}",
2681                        e
2682                    ))
2683                })?;
2684                Some(size)
2685            };
2686            Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
2687        }
2688        _ => Err(UnknownVtl2RelocationType(s.to_owned())),
2689    }
2690}
2691
2692#[derive(Debug, Copy, Clone, PartialEq)]
2693pub enum SmtConfigCli {
2694    Auto,
2695    Force,
2696    Off,
2697}
2698
2699#[derive(Debug, Error)]
2700#[error("expected auto, force, or off")]
2701pub struct BadSmtConfig;
2702
2703impl FromStr for SmtConfigCli {
2704    type Err = BadSmtConfig;
2705
2706    fn from_str(s: &str) -> Result<Self, Self::Err> {
2707        let r = match s {
2708            "auto" => Self::Auto,
2709            "force" => Self::Force,
2710            "off" => Self::Off,
2711            _ => return Err(BadSmtConfig),
2712        };
2713        Ok(r)
2714    }
2715}
2716
2717#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
2718fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
2719    let r = match s {
2720        "auto" => X2ApicConfig::Auto,
2721        "supported" => X2ApicConfig::Supported,
2722        "off" => X2ApicConfig::Unsupported,
2723        "on" => X2ApicConfig::Enabled,
2724        _ => return Err("expected auto, supported, off, or on"),
2725    };
2726    Ok(r)
2727}
2728
2729#[derive(Debug, Copy, Clone, ValueEnum)]
2730pub enum Vtl0LateMapPolicyCli {
2731    Off,
2732    Log,
2733    Halt,
2734    Exception,
2735}
2736
2737/// PCIe MSI controller selection for aarch64.
2738#[derive(Debug, Copy, Clone, Default, ValueEnum)]
2739pub enum GicMsiCli {
2740    /// Use ITS when available, fall back to GICv2m.
2741    #[default]
2742    Auto,
2743    /// Force GICv3 ITS (LPI-based MSIs).
2744    Its,
2745    /// Force GICv2m (SPI-based MSIs).
2746    V2m,
2747}
2748
2749#[derive(Debug, Copy, Clone, ValueEnum)]
2750pub enum IsolationCli {
2751    Vbs,
2752}
2753
2754#[derive(Debug, Copy, Clone, PartialEq)]
2755pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
2756
2757impl FromStr for PcatBootOrderCli {
2758    type Err = &'static str;
2759
2760    fn from_str(s: &str) -> Result<Self, Self::Err> {
2761        let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
2762        let mut order = Vec::new();
2763
2764        for item in s.split(',') {
2765            let device = match item {
2766                "optical" => PcatBootDevice::Optical,
2767                "hdd" => PcatBootDevice::HardDrive,
2768                "net" => PcatBootDevice::Network,
2769                "floppy" => PcatBootDevice::Floppy,
2770                _ => return Err("unknown boot device type"),
2771            };
2772
2773            let default_pos = default_order
2774                .iter()
2775                .position(|x| x == &Some(device))
2776                .ok_or("cannot pass duplicate boot devices")?;
2777
2778            order.push(default_order[default_pos].take().unwrap());
2779        }
2780
2781        order.extend(default_order.into_iter().flatten());
2782        assert_eq!(order.len(), 4);
2783
2784        Ok(Self(order.try_into().unwrap()))
2785    }
2786}
2787
2788#[derive(Copy, Clone, Debug, ValueEnum)]
2789pub enum UefiConsoleModeCli {
2790    Default,
2791    Com1,
2792    Com2,
2793    None,
2794}
2795
2796#[derive(Copy, Clone, Debug, Default, ValueEnum)]
2797pub enum EfiDiagnosticsLogLevelCli {
2798    #[default]
2799    Default,
2800    Info,
2801    Full,
2802}
2803
2804#[derive(Clone, Debug, PartialEq)]
2805pub struct PcieRootComplexCli {
2806    pub name: String,
2807    pub segment: u16,
2808    pub start_bus: u8,
2809    pub end_bus: u8,
2810    pub low_mmio: u32,
2811    pub high_mmio: u64,
2812    pub low_mmio_base: Option<u64>,
2813    pub high_mmio_base: Option<u64>,
2814    pub preserve_bars: bool,
2815    pub hdm: u64,
2816    pub hdm_window_restrictions: CfmwsWindowRestrictions,
2817    pub vnode: Option<u32>,
2818}
2819
2820/// A `0x`-prefixed hexadecimal address, used for MMIO base overrides.
2821struct HexAddress(u64);
2822
2823impl FromStr for HexAddress {
2824    type Err = anyhow::Error;
2825
2826    fn from_str(s: &str) -> anyhow::Result<Self> {
2827        Ok(HexAddress(parse_address(s)?))
2828    }
2829}
2830
2831/// A CFMWS window-restrictions bitmask (`0x21` or `33`).
2832struct CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions);
2833
2834impl FromStr for CfmwsWindowRestrictionsCli {
2835    type Err = anyhow::Error;
2836
2837    fn from_str(s: &str) -> anyhow::Result<Self> {
2838        Ok(CfmwsWindowRestrictionsCli(
2839            parse_cxl_cfmws_window_restriction_u16_bitmask(s)?,
2840        ))
2841    }
2842}
2843
2844/// Raw `--pcie-root-complex` options, parsed declaratively. Validated and
2845/// mapped into the public [`PcieRootComplexCli`] by its `FromStr`.
2846#[derive(vmm_cli::KeyValueArgs)]
2847struct PcieRootComplexArgs {
2848    #[kv(positional)]
2849    name: String,
2850    #[kv(default)]
2851    segment: u16,
2852    #[kv(default)]
2853    start_bus: u8,
2854    #[kv(default = 255)]
2855    end_bus: u8,
2856    #[kv(default = vmm_cli::MemorySize(64 * 1024 * 1024))]
2857    low_mmio: vmm_cli::MemorySize,
2858    #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
2859    high_mmio: vmm_cli::MemorySize,
2860    low_mmio_base: Option<HexAddress>,
2861    high_mmio_base: Option<HexAddress>,
2862    #[kv(flag)]
2863    preserve_bars: bool,
2864    #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
2865    hdm: vmm_cli::MemorySize,
2866    #[kv(default = CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions::DEVICE_COHERENT))]
2867    hdm_window_restrictions: CfmwsWindowRestrictionsCli,
2868    #[kv(key = "node")]
2869    vnode: Option<u32>,
2870}
2871
2872impl FromStr for PcieRootComplexCli {
2873    type Err = anyhow::Error;
2874
2875    fn from_str(s: &str) -> Result<Self, Self::Err> {
2876        let args: PcieRootComplexArgs = s.parse()?;
2877
2878        if args.start_bus > args.end_bus {
2879            anyhow::bail!("start_bus must be <= end_bus");
2880        }
2881
2882        let low_mmio = u32::try_from(args.low_mmio.0).context("low MMIO size exceeds 32 bits")?;
2883
2884        Ok(PcieRootComplexCli {
2885            name: args.name,
2886            segment: args.segment,
2887            start_bus: args.start_bus,
2888            end_bus: args.end_bus,
2889            low_mmio,
2890            high_mmio: args.high_mmio.0,
2891            low_mmio_base: args.low_mmio_base.map(|a| a.0),
2892            high_mmio_base: args.high_mmio_base.map(|a| a.0),
2893            preserve_bars: args.preserve_bars,
2894            hdm: args.hdm.0,
2895            hdm_window_restrictions: args.hdm_window_restrictions.0,
2896            vnode: args.vnode,
2897        })
2898    }
2899}
2900
2901fn parse_cxl_cfmws_window_restriction_u16_bitmask(
2902    s: &str,
2903) -> anyhow::Result<CfmwsWindowRestrictions> {
2904    let bits = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
2905        u16::from_str_radix(hex, 16).context("invalid hex bitmask")?
2906    } else {
2907        u16::from_str(s).context("invalid decimal bitmask")?
2908    };
2909
2910    CfmwsWindowRestrictions::try_from_bits(bits)
2911        .context("bitmask includes reserved CFMWS window restriction bits")
2912}
2913
2914#[derive(Clone, Debug, PartialEq)]
2915pub struct PcieRootPortCli {
2916    pub root_complex_name: String,
2917    pub name: String,
2918    pub devfn: Option<u8>,
2919    pub hotplug: bool,
2920    pub acs_capabilities_supported: Option<u16>,
2921    pub cxl: bool,
2922    pub pasid: bool,
2923}
2924
2925/// A colon-joined `parent:child` name pair used as the positional head of
2926/// `--pcie-root-port` and `--pcie-switch`.
2927struct PortNamePair {
2928    parent: String,
2929    child: String,
2930}
2931
2932impl FromStr for PortNamePair {
2933    type Err = anyhow::Error;
2934
2935    fn from_str(s: &str) -> anyhow::Result<Self> {
2936        let mut it = s.split(':');
2937        let parent = it
2938            .next()
2939            .filter(|x| !x.is_empty())
2940            .context("expected parent name")?;
2941        let child = it
2942            .next()
2943            .filter(|x| !x.is_empty())
2944            .context("expected child name")?;
2945        anyhow::ensure!(it.next().is_none(), "unexpected token in '{s}'");
2946        Ok(PortNamePair {
2947            parent: parent.to_string(),
2948            child: child.to_string(),
2949        })
2950    }
2951}
2952
2953/// A PCIe device/function address (`XX[.Y]`) parsed into a devfn.
2954struct PcieAddr(u8);
2955
2956impl FromStr for PcieAddr {
2957    type Err = anyhow::Error;
2958
2959    fn from_str(s: &str) -> anyhow::Result<Self> {
2960        Ok(PcieAddr(parse_pcie_addr(s)?))
2961    }
2962}
2963
2964/// An ACS capability bitmask (hex `0x..` or decimal).
2965struct AcsMask(u16);
2966
2967impl FromStr for AcsMask {
2968    type Err = anyhow::Error;
2969
2970    fn from_str(s: &str) -> anyhow::Result<Self> {
2971        Ok(AcsMask(parse_acs_capability_mask(s)?))
2972    }
2973}
2974
2975/// Raw `--pcie-root-port` options, mapped into [`PcieRootPortCli`].
2976#[derive(vmm_cli::KeyValueArgs)]
2977struct RootPortArgs {
2978    #[kv(positional)]
2979    names: PortNamePair,
2980    addr: Option<PcieAddr>,
2981    #[kv(flag)]
2982    hotplug: bool,
2983    acs: Option<AcsMask>,
2984    #[kv(flag)]
2985    cxl: bool,
2986    #[kv(flag)]
2987    pasid: bool,
2988}
2989
2990impl FromStr for PcieRootPortCli {
2991    type Err = anyhow::Error;
2992
2993    fn from_str(s: &str) -> Result<Self, Self::Err> {
2994        let args: RootPortArgs = s.parse()?;
2995        Ok(PcieRootPortCli {
2996            root_complex_name: args.names.parent,
2997            name: args.names.child,
2998            devfn: args.addr.map(|a| a.0),
2999            hotplug: args.hotplug,
3000            acs_capabilities_supported: args.acs.map(|a| a.0),
3001            cxl: args.cxl,
3002            pasid: args.pasid,
3003        })
3004    }
3005}
3006
3007/// Parses a PCIe address of the form `XX[.Y]`, where `XX` is the device number
3008/// (0-31) and the optional `Y` is the function number (0-7), into a devfn
3009/// (`device << 3 | function`).
3010fn parse_pcie_addr(s: &str) -> anyhow::Result<u8> {
3011    let parse_int = |v: &str| -> anyhow::Result<u8> {
3012        if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) {
3013            u8::from_str_radix(hex, 16).context("invalid hex number")
3014        } else {
3015            v.parse().context("invalid number")
3016        }
3017    };
3018
3019    let mut parts = s.split('.');
3020    let device = parse_int(parts.next().context("expected device number")?)?;
3021    let function = match parts.next() {
3022        Some(f) => parse_int(f)?,
3023        None => 0,
3024    };
3025    if parts.next().is_some() {
3026        anyhow::bail!("unexpected token in addr '{s}'");
3027    }
3028    if device > 31 {
3029        anyhow::bail!("device number {device} out of range (0-31)");
3030    }
3031    if function > 7 {
3032        anyhow::bail!("function number {function} out of range (0-7)");
3033    }
3034    Ok((device << 3) | function)
3035}
3036
3037#[derive(Clone, Debug, PartialEq)]
3038pub struct GenericPcieSwitchCli {
3039    pub port_name: String,
3040    pub name: String,
3041    pub num_downstream_ports: u8,
3042    pub hotplug: bool,
3043    pub acs_capabilities_supported: Option<u16>,
3044    pub pasid: bool,
3045}
3046
3047/// Raw `--pcie-switch` options, mapped into [`GenericPcieSwitchCli`].
3048#[derive(vmm_cli::KeyValueArgs)]
3049struct SwitchArgs {
3050    #[kv(positional)]
3051    names: PortNamePair,
3052    #[kv(default = 4)]
3053    num_downstream_ports: u8,
3054    #[kv(flag)]
3055    hotplug: bool,
3056    acs: Option<AcsMask>,
3057    #[kv(flag)]
3058    pasid: bool,
3059}
3060
3061impl FromStr for GenericPcieSwitchCli {
3062    type Err = anyhow::Error;
3063
3064    fn from_str(s: &str) -> Result<Self, Self::Err> {
3065        let args: SwitchArgs = s.parse()?;
3066        Ok(GenericPcieSwitchCli {
3067            port_name: args.names.parent,
3068            name: args.names.child,
3069            num_downstream_ports: args.num_downstream_ports,
3070            hotplug: args.hotplug,
3071            acs_capabilities_supported: args.acs.map(|a| a.0),
3072            pasid: args.pasid,
3073        })
3074    }
3075}
3076
3077/// CLI configuration mapping a PCIe port name to a generic-initiator NUMA node.
3078#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3079pub struct PcieGenericInitiatorCli {
3080    /// Name of the PCIe port (root port or switch downstream port) behind
3081    /// which the generic-initiator device resides.
3082    #[kv(key = "port")]
3083    pub port_name: String,
3084    /// NUMA node the device is a generic initiator for.
3085    pub node: u32,
3086}
3087
3088/// CLI configuration for a PCIe remote device.
3089#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3090pub struct PcieRemoteCli {
3091    /// Name of the PCIe downstream port to attach to.
3092    #[kv(positional)]
3093    pub port_name: String,
3094    /// TCP socket address for the remote simulator.
3095    #[kv(key = "socket")]
3096    pub socket_addr: Option<String>,
3097    /// Hardware unit identifier for plug request.
3098    #[kv(default)]
3099    pub hu: u16,
3100    /// Controller identifier for plug request.
3101    #[kv(default)]
3102    pub controller: u16,
3103}
3104
3105/// CLI configuration for a VFIO-assigned PCI device.
3106///
3107/// Syntax: `host=<bdf>,port=<name>[,iommu=<id>][,barN=host|barN=0x<addr>]`
3108#[cfg(target_os = "linux")]
3109#[derive(Clone, Debug)]
3110pub struct VfioDeviceCli {
3111    /// Name of the PCIe downstream port to attach to.
3112    pub port_name: String,
3113    /// PCI BDF address of the device on the host (e.g., "0000:01:00.0").
3114    pub pci_id: String,
3115    /// Optional iommufd context ID. When set, uses VFIO cdev + iommufd
3116    /// instead of the legacy group/container path.
3117    pub iommu: Option<String>,
3118    /// Per-BAR pre-programming configuration.
3119    pub bar_addresses: [vfio_assigned_device_resources::BarAddressConfig; 6],
3120}
3121
3122/// Per-BAR address configuration parsed from the CLI.
3123#[cfg(target_os = "linux")]
3124struct BarAddressCli(vfio_assigned_device_resources::BarAddressConfig);
3125
3126#[cfg(target_os = "linux")]
3127impl FromStr for BarAddressCli {
3128    type Err = anyhow::Error;
3129
3130    fn from_str(s: &str) -> anyhow::Result<Self> {
3131        let config = if s == "host" {
3132            vfio_assigned_device_resources::BarAddressConfig::HostAssigned
3133        } else if let Some(value) = s.strip_prefix("0x") {
3134            let address = u64::from_str_radix(value, 16).context("invalid BAR address")?;
3135            anyhow::ensure!(address != 0, "BAR address must be nonzero");
3136            vfio_assigned_device_resources::BarAddressConfig::Fixed(address)
3137        } else {
3138            anyhow::bail!("expected 'host' or a hexadecimal address starting with '0x'");
3139        };
3140        Ok(Self(config))
3141    }
3142}
3143
3144/// Per-BAR address configuration, flattened into
3145/// [`VfioArgs`].
3146#[cfg(target_os = "linux")]
3147#[derive(vmm_cli::KeyValueArgs)]
3148struct BarFlags {
3149    bar0: Option<BarAddressCli>,
3150    bar1: Option<BarAddressCli>,
3151    bar2: Option<BarAddressCli>,
3152    bar3: Option<BarAddressCli>,
3153    bar4: Option<BarAddressCli>,
3154    bar5: Option<BarAddressCli>,
3155}
3156
3157/// Raw `--vfio` options, resolved and validated into a [`VfioDeviceCli`].
3158#[cfg(target_os = "linux")]
3159#[derive(vmm_cli::KeyValueArgs)]
3160struct VfioArgs {
3161    host: String,
3162    port: String,
3163    iommu: Option<String>,
3164    #[kv(flatten)]
3165    bars: BarFlags,
3166}
3167
3168#[cfg(target_os = "linux")]
3169impl FromStr for VfioDeviceCli {
3170    type Err = anyhow::Error;
3171
3172    fn from_str(s: &str) -> Result<Self, Self::Err> {
3173        let args: VfioArgs = s.parse()?;
3174
3175        // Reject path separators to prevent sysfs path traversal via Path::join.
3176        if args.host.contains('/') || args.host.contains("..") {
3177            anyhow::bail!("PCI address must not contain path separators");
3178        }
3179
3180        let bars = args.bars;
3181        let bar_addresses = [
3182            bars.bar0.map(|bar| bar.0).unwrap_or_default(),
3183            bars.bar1.map(|bar| bar.0).unwrap_or_default(),
3184            bars.bar2.map(|bar| bar.0).unwrap_or_default(),
3185            bars.bar3.map(|bar| bar.0).unwrap_or_default(),
3186            bars.bar4.map(|bar| bar.0).unwrap_or_default(),
3187            bars.bar5.map(|bar| bar.0).unwrap_or_default(),
3188        ];
3189
3190        Ok(VfioDeviceCli {
3191            port_name: args.port,
3192            pci_id: args.host,
3193            iommu: args.iommu,
3194            bar_addresses,
3195        })
3196    }
3197}
3198
3199/// CLI configuration for an SMMUv3 instance.
3200///
3201/// Syntax: `rc=<name>[,accel][,oas=auto|N]`. `oas` defaults to `auto`.
3202#[cfg(guest_arch = "aarch64")]
3203#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
3204pub struct SmmuCli {
3205    /// Name of the PCIe root complex this SMMU covers.
3206    #[kv(key = "rc")]
3207    pub rc_name: String,
3208    /// Enable HW-accelerated nested translation (iommufd).
3209    #[kv(flag)]
3210    pub accel: bool,
3211    /// Output address size policy.
3212    #[kv(default)]
3213    pub oas: SmmuOasCli,
3214}
3215
3216/// Output address size (OAS) policy parsed from `--smmu`.
3217#[cfg(guest_arch = "aarch64")]
3218#[derive(Clone, Copy, Debug, Default)]
3219pub enum SmmuOasCli {
3220    /// Advertise a fixed default OAS (see the `--smmu` docs for the sizing
3221    /// policy and when a larger fixed OAS is required).
3222    #[default]
3223    Auto,
3224    /// Fixed OAS in bits (one of 32, 36, 40, 42, 44, 48, 52).
3225    Fixed(u8),
3226}
3227
3228#[cfg(guest_arch = "aarch64")]
3229impl FromStr for SmmuOasCli {
3230    type Err = anyhow::Error;
3231
3232    fn from_str(s: &str) -> Result<Self, Self::Err> {
3233        Ok(if s == "auto" {
3234            SmmuOasCli::Auto
3235        } else {
3236            SmmuOasCli::Fixed(s.parse().context("oas must be 'auto' or a number")?)
3237        })
3238    }
3239}
3240
3241/// CLI configuration for an iommufd context.
3242///
3243/// Syntax: `id=<name>`
3244#[cfg(target_os = "linux")]
3245#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
3246pub struct IommuCli {
3247    /// Unique identifier for this iommufd context.
3248    pub id: String,
3249}
3250
3251/// Read a environment variable that may / may-not have a target-specific
3252/// prefix. e.g: `default_value_from_arch_env("FOO")` would first try and read
3253/// from `FOO`, and if that's not found, it will try `X86_64_FOO`.
3254///
3255/// Must return an `OsString`, in order to be compatible with `clap`'s
3256/// default_value code. As such - to encode the absence of the env-var, an empty
3257/// OsString is returned.
3258fn default_value_from_arch_env(name: &str) -> OsString {
3259    let prefix = if cfg!(guest_arch = "x86_64") {
3260        "X86_64"
3261    } else if cfg!(guest_arch = "aarch64") {
3262        "AARCH64"
3263    } else {
3264        return Default::default();
3265    };
3266    let prefixed = format!("{}_{}", prefix, name);
3267    std::env::var_os(name)
3268        .or_else(|| std::env::var_os(prefixed))
3269        .unwrap_or_default()
3270}
3271
3272/// Workaround to use `Option<PathBuf>` alongside [`default_value_from_arch_env`]
3273#[derive(Clone)]
3274pub struct OptionalPathBuf(pub Option<PathBuf>);
3275
3276impl From<&std::ffi::OsStr> for OptionalPathBuf {
3277    fn from(s: &std::ffi::OsStr) -> Self {
3278        OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
3279    }
3280}
3281
3282#[cfg(target_os = "linux")]
3283#[derive(Clone)]
3284pub enum VhostUserDeviceTypeCli {
3285    /// Block device — config from backend via GET_CONFIG, with num_queues
3286    /// patched by the frontend.
3287    Blk {
3288        num_queues: Option<u16>,
3289        queue_size: Option<u16>,
3290    },
3291    /// Filesystem device — frontend-owned config with mount tag.
3292    Fs {
3293        tag: String,
3294        num_queues: Option<u16>,
3295        queue_size: Option<u16>,
3296    },
3297    /// Generic device identified by numeric virtio device ID.
3298    Other {
3299        device_id: u16,
3300        queue_sizes: Vec<u16>,
3301    },
3302}
3303
3304#[cfg(target_os = "linux")]
3305#[derive(Clone)]
3306pub struct VhostUserCli {
3307    pub socket_path: String,
3308    pub device_type: VhostUserDeviceTypeCli,
3309    pub pcie_port: Option<String>,
3310}
3311
3312/// Raw `--vhost-user` options, resolved into a [`VhostUserCli`] by its
3313/// `FromStr`.
3314#[cfg(target_os = "linux")]
3315#[derive(vmm_cli::KeyValueArgs)]
3316struct VhostUserArgs {
3317    #[kv(positional)]
3318    socket_path: String,
3319    #[kv(key = "type")]
3320    type_name: Option<String>,
3321    device_id: Option<u16>,
3322    tag: Option<String>,
3323    pcie_port: Option<String>,
3324    num_queues: Option<u16>,
3325    queue_size: Option<u16>,
3326    queue_sizes: Option<vmm_cli::BracketList<u16>>,
3327}
3328
3329#[cfg(target_os = "linux")]
3330impl FromStr for VhostUserCli {
3331    type Err = anyhow::Error;
3332
3333    fn from_str(s: &str) -> anyhow::Result<Self> {
3334        let mut args: VhostUserArgs = s.parse()?;
3335        let type_name = args.type_name.take();
3336
3337        if type_name.is_some() == args.device_id.is_some() {
3338            anyhow::bail!("must specify type=<name> or device_id=<N>");
3339        }
3340
3341        // Each variant consumes the options it accepts; whatever is left over
3342        // afterward was used with the wrong device type.
3343        let device_type = match type_name.as_deref() {
3344            Some("fs") => VhostUserDeviceTypeCli::Fs {
3345                tag: args.tag.take().context("type=fs requires tag=<name>")?,
3346                num_queues: args.num_queues.take(),
3347                queue_size: args.queue_size.take(),
3348            },
3349            Some("blk") => VhostUserDeviceTypeCli::Blk {
3350                num_queues: args.num_queues.take(),
3351                queue_size: args.queue_size.take(),
3352            },
3353            Some(ty) => anyhow::bail!("unknown vhost-user device type: '{ty}'"),
3354            None => {
3355                let queue_sizes = args
3356                    .queue_sizes
3357                    .take()
3358                    .context("device_id= requires queue_sizes=[N,N,...]")?
3359                    .0;
3360                anyhow::ensure!(!queue_sizes.is_empty(), "queue_sizes must be non-empty");
3361                VhostUserDeviceTypeCli::Other {
3362                    device_id: args.device_id.unwrap(),
3363                    queue_sizes,
3364                }
3365            }
3366        };
3367
3368        if args.tag.is_some() {
3369            anyhow::bail!("tag= is only valid for type=fs");
3370        }
3371        if args.queue_sizes.is_some() {
3372            anyhow::bail!("queue_sizes= is only valid for device_id=");
3373        }
3374        if args.num_queues.is_some() || args.queue_size.is_some() {
3375            anyhow::bail!(
3376                "num_queues= and queue_size= are not valid for device_id=; use queue_sizes="
3377            );
3378        }
3379
3380        Ok(VhostUserCli {
3381            socket_path: args.socket_path,
3382            device_type,
3383            pcie_port: args.pcie_port,
3384        })
3385    }
3386}
3387
3388#[cfg(test)]
3389mod tests {
3390    use super::*;
3391
3392    use std::path::Path;
3393    use test_with_tracing::test;
3394
3395    #[test]
3396    fn test_parse_rpc() {
3397        // explicit path, default transport
3398        let rpc = RpcCli::from_str("path=/tmp/openvmm.sock").unwrap();
3399        assert_eq!(rpc.path, Path::new("/tmp/openvmm.sock"));
3400        assert_eq!(rpc.transport, RpcTransportCli::Auto);
3401
3402        // explicit transport
3403        for (s, transport) in [
3404            ("auto", RpcTransportCli::Auto),
3405            ("ttrpc", RpcTransportCli::Ttrpc),
3406            ("grpc", RpcTransportCli::Grpc),
3407        ] {
3408            let rpc = RpcCli::from_str(&format!("path=/tmp/s.sock,transport={s}")).unwrap();
3409            assert_eq!(rpc.path, Path::new("/tmp/s.sock"));
3410            assert_eq!(rpc.transport, transport);
3411        }
3412
3413        // errors
3414        assert!(RpcCli::from_str("").is_err());
3415        assert!(RpcCli::from_str("transport=ttrpc").is_err());
3416        assert!(RpcCli::from_str("path=").is_err());
3417        assert!(RpcCli::from_str("path=/tmp/s.sock,transport=bogus").is_err());
3418        assert!(RpcCli::from_str("path=/tmp/s.sock,bogus=1").is_err());
3419        assert!(RpcCli::from_str("path=/a,path=/b").is_err());
3420    }
3421
3422    #[test]
3423    fn test_parse_file_opts() {
3424        // file: prefix with create
3425        let disk = DiskCliKind::from_str("file:test.vhd;create=1G").unwrap();
3426        assert!(matches!(
3427            &disk,
3428            DiskCliKind::File { path, create_with_len: Some(len), direct: false }
3429                if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
3430        ));
3431
3432        // bare path with create (no file: prefix)
3433        let disk = DiskCliKind::from_str("test.vhd;create=1G").unwrap();
3434        assert!(matches!(
3435            &disk,
3436            DiskCliKind::File { path, create_with_len: Some(len), direct: false }
3437                if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
3438        ));
3439
3440        // direct flag
3441        let disk = DiskCliKind::from_str("file:/dev/sdb;direct").unwrap();
3442        assert!(matches!(
3443            &disk,
3444            DiskCliKind::File { path, create_with_len: None, direct: true }
3445                if path == Path::new("/dev/sdb")
3446        ));
3447
3448        // direct + create in either order
3449        let disk = DiskCliKind::from_str("file:disk.img;direct;create=1G").unwrap();
3450        assert!(matches!(
3451            &disk,
3452            DiskCliKind::File { path, create_with_len: Some(len), direct: true }
3453                if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
3454        ));
3455
3456        let disk = DiskCliKind::from_str("file:disk.img;create=1G;direct").unwrap();
3457        assert!(matches!(
3458            &disk,
3459            DiskCliKind::File { path, create_with_len: Some(len), direct: true }
3460                if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
3461        ));
3462
3463        // plain path, no options
3464        let disk = DiskCliKind::from_str("file:disk.img").unwrap();
3465        assert!(matches!(
3466            &disk,
3467            DiskCliKind::File { path, create_with_len: None, direct: false }
3468                if path == Path::new("disk.img")
3469        ));
3470
3471        // invalid option rejected
3472        assert!(DiskCliKind::from_str("file:disk.img;bogus").is_err());
3473
3474        // direct rejected for sql disks
3475        assert!(DiskCliKind::from_str("sql:db.sqlite;direct").is_err());
3476    }
3477
3478    #[test]
3479    fn test_parse_memory_disk() {
3480        let s = "mem:1G";
3481        let disk = DiskCliKind::from_str(s).unwrap();
3482        match disk {
3483            DiskCliKind::Memory(size) => {
3484                assert_eq!(size, 1024 * 1024 * 1024); // 1G
3485            }
3486            _ => panic!("Expected Memory variant"),
3487        }
3488    }
3489
3490    #[test]
3491    fn test_parse_pcie_disk() {
3492        assert_eq!(
3493            DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
3494            Some("p0".to_string())
3495        );
3496        assert_eq!(
3497            DiskCli::from_str("file:path.vhdx,pcie_port=p0")
3498                .unwrap()
3499                .pcie_port,
3500            Some("p0".to_string())
3501        );
3502        assert_eq!(
3503            DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
3504                .unwrap()
3505                .pcie_port,
3506            Some("p0".to_string())
3507        );
3508
3509        // Missing port name
3510        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
3511
3512        // Incompatible with various other disk fields
3513        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
3514        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
3515        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
3516    }
3517
3518    #[test]
3519    fn test_parse_memory_diff_disk() {
3520        let s = "memdiff:file:base.img";
3521        let disk = DiskCliKind::from_str(s).unwrap();
3522        match disk {
3523            DiskCliKind::MemoryDiff(inner) => match *inner {
3524                DiskCliKind::File {
3525                    path,
3526                    create_with_len,
3527                    ..
3528                } => {
3529                    assert_eq!(path, PathBuf::from("base.img"));
3530                    assert_eq!(create_with_len, None);
3531                }
3532                _ => panic!("Expected File variant inside MemoryDiff"),
3533            },
3534            _ => panic!("Expected MemoryDiff variant"),
3535        }
3536    }
3537
3538    #[test]
3539    fn test_parse_sqlite_disk() {
3540        let s = "sql:db.sqlite;create=2G";
3541        let disk = DiskCliKind::from_str(s).unwrap();
3542        match disk {
3543            DiskCliKind::Sqlite {
3544                path,
3545                create_with_len,
3546            } => {
3547                assert_eq!(path, PathBuf::from("db.sqlite"));
3548                assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
3549            }
3550            _ => panic!("Expected Sqlite variant"),
3551        }
3552
3553        // Test without create option
3554        let s = "sql:db.sqlite";
3555        let disk = DiskCliKind::from_str(s).unwrap();
3556        match disk {
3557            DiskCliKind::Sqlite {
3558                path,
3559                create_with_len,
3560            } => {
3561                assert_eq!(path, PathBuf::from("db.sqlite"));
3562                assert_eq!(create_with_len, None);
3563            }
3564            _ => panic!("Expected Sqlite variant"),
3565        }
3566    }
3567
3568    #[test]
3569    fn test_parse_sqlite_diff_disk() {
3570        // Test with create option
3571        let s = "sqldiff:diff.sqlite;create:file:base.img";
3572        let disk = DiskCliKind::from_str(s).unwrap();
3573        match disk {
3574            DiskCliKind::SqliteDiff { path, create, disk } => {
3575                assert_eq!(path, PathBuf::from("diff.sqlite"));
3576                assert!(create);
3577                match *disk {
3578                    DiskCliKind::File {
3579                        path,
3580                        create_with_len,
3581                        ..
3582                    } => {
3583                        assert_eq!(path, PathBuf::from("base.img"));
3584                        assert_eq!(create_with_len, None);
3585                    }
3586                    _ => panic!("Expected File variant inside SqliteDiff"),
3587                }
3588            }
3589            _ => panic!("Expected SqliteDiff variant"),
3590        }
3591
3592        // Test without create option
3593        let s = "sqldiff:diff.sqlite:file:base.img";
3594        let disk = DiskCliKind::from_str(s).unwrap();
3595        match disk {
3596            DiskCliKind::SqliteDiff { path, create, disk } => {
3597                assert_eq!(path, PathBuf::from("diff.sqlite"));
3598                assert!(!create);
3599                match *disk {
3600                    DiskCliKind::File {
3601                        path,
3602                        create_with_len,
3603                        ..
3604                    } => {
3605                        assert_eq!(path, PathBuf::from("base.img"));
3606                        assert_eq!(create_with_len, None);
3607                    }
3608                    _ => panic!("Expected File variant inside SqliteDiff"),
3609                }
3610            }
3611            _ => panic!("Expected SqliteDiff variant"),
3612        }
3613    }
3614
3615    #[test]
3616    fn test_parse_autocache_sqlite_disk() {
3617        // Test with cache path provided
3618        let disk =
3619            DiskCliKind::parse_autocache(":file:disk.vhd", Ok("/tmp/cache".to_string())).unwrap();
3620        assert!(matches!(
3621            disk,
3622            DiskCliKind::AutoCacheSqlite {
3623                cache_path,
3624                key,
3625                disk: _disk,
3626            } if cache_path == "/tmp/cache" && key.is_none()
3627        ));
3628
3629        // Test with key
3630        let disk =
3631            DiskCliKind::parse_autocache("mykey:file:disk.vhd", Ok("/tmp/cache".to_string()))
3632                .unwrap();
3633        assert!(matches!(
3634            disk,
3635            DiskCliKind::AutoCacheSqlite {
3636                cache_path,
3637                key: Some(key),
3638                disk: _disk,
3639            } if cache_path == "/tmp/cache" && key == "mykey"
3640        ));
3641
3642        // Test without cache path
3643        assert!(
3644            DiskCliKind::parse_autocache(":file:disk.vhd", Err(std::env::VarError::NotPresent),)
3645                .is_err()
3646        );
3647    }
3648
3649    #[test]
3650    fn test_parse_disk_errors() {
3651        assert!(DiskCliKind::from_str("invalid:").is_err());
3652        assert!(DiskCliKind::from_str("memory:extra").is_err());
3653
3654        // Test sqlite: without environment variable
3655        assert!(DiskCliKind::from_str("sqlite:").is_err());
3656    }
3657
3658    #[test]
3659    fn test_parse_errors() {
3660        // Invalid memory size
3661        assert!(DiskCliKind::from_str("mem:invalid").is_err());
3662
3663        // Invalid syntax for SQLiteDiff
3664        assert!(DiskCliKind::from_str("sqldiff:path").is_err());
3665
3666        // Missing OPENVMM_AUTO_CACHE_PATH for AutoCacheSqlite
3667        assert!(
3668            DiskCliKind::parse_autocache("key:file:disk.vhd", Err(std::env::VarError::NotPresent),)
3669                .is_err()
3670        );
3671
3672        // Invalid blob kind
3673        assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
3674
3675        // Invalid cipher
3676        assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
3677
3678        // Invalid format for crypt (missing parts)
3679        assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
3680
3681        // Invalid disk kind
3682        assert!(DiskCliKind::from_str("invalid:path").is_err());
3683
3684        // Missing create size
3685        assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
3686    }
3687
3688    #[test]
3689    fn test_fs_args_from_str() {
3690        let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
3691        assert_eq!(args.tag, "tag1");
3692        assert_eq!(args.path, "/path/to/fs");
3693
3694        // Test error cases
3695        assert!(FsArgs::from_str("tag1").is_err());
3696        assert!(FsArgs::from_str("tag1,/path,extra").is_err());
3697    }
3698
3699    #[test]
3700    fn test_fs_args_with_options_from_str() {
3701        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
3702        assert_eq!(args.tag, "tag1");
3703        assert_eq!(args.path, "/path/to/fs");
3704        assert_eq!(args.options, "opt1;opt2");
3705
3706        // Test without options
3707        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
3708        assert_eq!(args.tag, "tag1");
3709        assert_eq!(args.path, "/path/to/fs");
3710        assert_eq!(args.options, "");
3711
3712        // Test error case
3713        assert!(FsArgsWithOptions::from_str("tag1").is_err());
3714    }
3715
3716    #[test]
3717    fn test_serial_config_from_str() {
3718        assert_eq!(
3719            SerialConfigCli::from_str("none").unwrap(),
3720            SerialConfigCli::None
3721        );
3722        assert_eq!(
3723            SerialConfigCli::from_str("console").unwrap(),
3724            SerialConfigCli::Console
3725        );
3726        assert_eq!(
3727            SerialConfigCli::from_str("stderr").unwrap(),
3728            SerialConfigCli::Stderr
3729        );
3730
3731        // Test file config
3732        let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
3733        if let SerialConfigCli::File(path) = file_config {
3734            assert_eq!(path.to_str().unwrap(), "/path/to/file");
3735        } else {
3736            panic!("Expected File variant");
3737        }
3738
3739        // Test term config with name, but no specific path
3740        match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
3741            SerialConfigCli::NewConsole(None, Some(name)) => {
3742                assert_eq!(name, "MyTerm");
3743            }
3744            _ => panic!("Expected NewConsole variant with name"),
3745        }
3746
3747        // Test term config without name, but no specific path
3748        match SerialConfigCli::from_str("term").unwrap() {
3749            SerialConfigCli::NewConsole(None, None) => (),
3750            _ => panic!("Expected NewConsole variant without name"),
3751        }
3752
3753        // Test term config with name
3754        match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
3755            SerialConfigCli::NewConsole(Some(path), Some(name)) => {
3756                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
3757                assert_eq!(name, "MyTerm");
3758            }
3759            _ => panic!("Expected NewConsole variant with name"),
3760        }
3761
3762        // Test term config without name
3763        match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
3764            SerialConfigCli::NewConsole(Some(path), None) => {
3765                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
3766            }
3767            _ => panic!("Expected NewConsole variant without name"),
3768        }
3769
3770        // Test TCP config
3771        match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
3772            SerialConfigCli::Tcp(addr) => {
3773                assert_eq!(addr.to_string(), "127.0.0.1:1234");
3774            }
3775            _ => panic!("Expected Tcp variant"),
3776        }
3777
3778        // Test pipe config
3779        match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
3780            SerialConfigCli::Pipe(path) => {
3781                assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
3782            }
3783            _ => panic!("Expected Pipe variant"),
3784        }
3785
3786        // Test error cases
3787        assert!(SerialConfigCli::from_str("").is_err());
3788        assert!(SerialConfigCli::from_str("unknown").is_err());
3789        assert!(SerialConfigCli::from_str("file").is_err());
3790        assert!(SerialConfigCli::from_str("listen").is_err());
3791    }
3792
3793    #[test]
3794    fn test_endpoint_config_from_str() {
3795        // Test none
3796        assert!(matches!(
3797            EndpointConfigCli::from_str("none").unwrap(),
3798            EndpointConfigCli::None
3799        ));
3800
3801        // Test consomme without cidr
3802        match EndpointConfigCli::from_str("consomme").unwrap() {
3803            EndpointConfigCli::Consomme {
3804                cidr: None,
3805                host_fwd,
3806            } => assert!(host_fwd.is_empty()),
3807            _ => panic!("Expected Consomme variant without cidr"),
3808        }
3809
3810        // Test consomme with cidr
3811        match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
3812            EndpointConfigCli::Consomme {
3813                cidr: Some(cidr),
3814                host_fwd,
3815            } => {
3816                assert_eq!(cidr, "192.168.0.0/24");
3817                assert!(host_fwd.is_empty());
3818            }
3819            _ => panic!("Expected Consomme variant with cidr"),
3820        }
3821
3822        // Test consomme with hostfwd
3823        match EndpointConfigCli::from_str("consomme:hostfwd=udp:127.0.0.1:5000-:5000").unwrap() {
3824            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3825                assert!(cidr.is_none());
3826                assert_eq!(host_fwd.len(), 1);
3827                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Udp);
3828                assert_eq!(
3829                    host_fwd[0].host_address,
3830                    Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
3831                );
3832                assert_eq!(host_fwd[0].host_port, 5000);
3833                assert_eq!(host_fwd[0].guest_port, 5000);
3834            }
3835            _ => panic!("Expected Consomme variant with hostfwd"),
3836        }
3837
3838        // Test consomme with cidr and hostfwd
3839        match EndpointConfigCli::from_str("consomme:10.0.0.0/24,hostfwd=tcp::2222-:22").unwrap() {
3840            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3841                assert_eq!(cidr.as_deref(), Some("10.0.0.0/24"));
3842                assert_eq!(host_fwd.len(), 1);
3843                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3844                assert_eq!(host_fwd[0].host_port, 2222);
3845                assert_eq!(host_fwd[0].guest_port, 22);
3846            }
3847            _ => panic!("Expected Consomme variant with cidr and hostfwd"),
3848        }
3849
3850        // Test consomme with multiple hostfwd
3851        match EndpointConfigCli::from_str("consomme:hostfwd=tcp::2222-:22,hostfwd=tcp::3389-:3389")
3852            .unwrap()
3853        {
3854            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3855                assert!(cidr.is_none());
3856                assert_eq!(host_fwd.len(), 2);
3857                assert_eq!(host_fwd[0].host_port, 2222);
3858                assert_eq!(host_fwd[0].guest_port, 22);
3859                assert_eq!(host_fwd[1].host_port, 3389);
3860                assert_eq!(host_fwd[1].guest_port, 3389);
3861            }
3862            _ => panic!("Expected Consomme variant with multiple hostfwd"),
3863        }
3864
3865        // Test consomme with different host and guest ports
3866        match EndpointConfigCli::from_str("consomme:hostfwd=tcp:127.0.0.1:8080-:80").unwrap() {
3867            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3868                assert!(cidr.is_none());
3869                assert_eq!(host_fwd.len(), 1);
3870                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3871                assert_eq!(
3872                    host_fwd[0].host_address,
3873                    Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
3874                );
3875                assert_eq!(host_fwd[0].host_port, 8080);
3876                assert_eq!(host_fwd[0].guest_port, 80);
3877            }
3878            _ => panic!("Expected Consomme variant with host/guest port mapping"),
3879        }
3880
3881        // Test consomme with guest address (accepted but ignored by backend)
3882        match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-10.0.0.2:80").unwrap() {
3883            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3884                assert!(cidr.is_none());
3885                assert_eq!(host_fwd[0].host_port, 8080);
3886                assert_eq!(host_fwd[0].guest_port, 80);
3887            }
3888            _ => panic!("Expected Consomme variant with guest address"),
3889        }
3890
3891        // Test consomme with IPv6 host address (bracketed)
3892        match EndpointConfigCli::from_str("consomme:hostfwd=tcp:[::1]:8080-:80").unwrap() {
3893            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3894                assert!(cidr.is_none());
3895                assert_eq!(host_fwd.len(), 1);
3896                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3897                assert_eq!(
3898                    host_fwd[0].host_address,
3899                    Some(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST))
3900                );
3901                assert_eq!(host_fwd[0].host_port, 8080);
3902                assert_eq!(host_fwd[0].guest_port, 80);
3903            }
3904            _ => panic!("Expected Consomme variant with IPv6 hostfwd"),
3905        }
3906
3907        // Test consomme with IPv6 guest address (bracketed)
3908        match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-[::1]:80").unwrap() {
3909            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3910                assert!(cidr.is_none());
3911                assert_eq!(host_fwd[0].host_port, 8080);
3912                assert_eq!(host_fwd[0].guest_port, 80);
3913            }
3914            _ => panic!("Expected Consomme variant with IPv6 guest address"),
3915        }
3916
3917        // Test dio without id
3918        match EndpointConfigCli::from_str("dio").unwrap() {
3919            EndpointConfigCli::Dio { id: None } => (),
3920            _ => panic!("Expected Dio variant without id"),
3921        }
3922
3923        // Test dio with id
3924        match EndpointConfigCli::from_str("dio:test_id").unwrap() {
3925            EndpointConfigCli::Dio { id: Some(id) } => {
3926                assert_eq!(id, "test_id");
3927            }
3928            _ => panic!("Expected Dio variant with id"),
3929        }
3930
3931        // Test tap
3932        match EndpointConfigCli::from_str("tap:tap0").unwrap() {
3933            EndpointConfigCli::Tap { name } => {
3934                assert_eq!(name, "tap0");
3935            }
3936            _ => panic!("Expected Tap variant"),
3937        }
3938
3939        // Test error case
3940        assert!(EndpointConfigCli::from_str("invalid").is_err());
3941    }
3942
3943    #[test]
3944    fn test_nic_config_from_str() {
3945        use openvmm_defs::config::DeviceVtl;
3946
3947        // Test basic endpoint
3948        let config = NicConfigCli::from_str("none").unwrap();
3949        assert_eq!(config.vtl, DeviceVtl::Vtl0);
3950        assert!(config.max_queues.is_none());
3951        assert!(!config.underhill);
3952        assert!(config.pcie_port.is_none());
3953        assert!(matches!(config.endpoint, EndpointConfigCli::None));
3954
3955        // Test with vtl2
3956        let config = NicConfigCli::from_str("vtl2:none").unwrap();
3957        assert_eq!(config.vtl, DeviceVtl::Vtl2);
3958        assert!(config.pcie_port.is_none());
3959        assert!(matches!(config.endpoint, EndpointConfigCli::None));
3960
3961        // Test with queues
3962        let config = NicConfigCli::from_str("queues=4:none").unwrap();
3963        assert_eq!(config.max_queues, Some(4));
3964        assert!(config.pcie_port.is_none());
3965        assert!(matches!(config.endpoint, EndpointConfigCli::None));
3966
3967        // Test with underhill
3968        let config = NicConfigCli::from_str("uh:none").unwrap();
3969        assert!(config.underhill);
3970        assert!(config.pcie_port.is_none());
3971        assert!(matches!(config.endpoint, EndpointConfigCli::None));
3972
3973        // Test with pcie_port
3974        let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
3975        assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
3976        assert!(matches!(config.endpoint, EndpointConfigCli::None));
3977
3978        // Test error cases
3979        assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
3980        assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); // uh incompatible with vtl2
3981        assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
3982        assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
3983        assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
3984        assert!(NicConfigCli::from_str("pcie_port:none").is_err());
3985    }
3986
3987    #[test]
3988    fn test_parse_pcie_port_prefix() {
3989        // Successful prefix parsing
3990        let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
3991        assert_eq!(port.unwrap(), "rp0");
3992        assert_eq!(rest, "tag,path");
3993
3994        // No prefix
3995        let (port, rest) = parse_pcie_port_prefix("tag,path");
3996        assert!(port.is_none());
3997        assert_eq!(rest, "tag,path");
3998
3999        // Empty port name — not parsed as a prefix
4000        let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
4001        assert!(port.is_none());
4002        assert_eq!(rest, "pcie_port=:tag,path");
4003
4004        // Missing colon — not parsed as a prefix
4005        let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
4006        assert!(port.is_none());
4007        assert_eq!(rest, "pcie_port=rp0");
4008    }
4009
4010    #[test]
4011    fn test_cxl_test_device_cli_parse_valid() {
4012        let cfg = CxlTestDeviceCli::from_str("mem:1G,pcie_port=rp0").unwrap();
4013        assert_eq!(cfg.hdm_size, 1024 * 1024 * 1024);
4014        assert_eq!(cfg.pcie_port, "rp0");
4015    }
4016
4017    #[test]
4018    fn test_cxl_test_device_cli_parse_invalid() {
4019        assert!(CxlTestDeviceCli::from_str("file:disk.img,pcie_port=rp0").is_err());
4020        assert!(CxlTestDeviceCli::from_str("mem:1G").is_err());
4021        assert!(CxlTestDeviceCli::from_str("mem:1G,pcie_port=").is_err());
4022    }
4023
4024    #[test]
4025    fn test_fs_args_pcie_port() {
4026        // Without pcie_port
4027        let args = FsArgs::from_str("myfs,/path").unwrap();
4028        assert_eq!(args.tag, "myfs");
4029        assert_eq!(args.path, "/path");
4030        assert!(args.pcie_port.is_none());
4031
4032        // With pcie_port
4033        let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
4034        assert_eq!(args.pcie_port.unwrap(), "rp0");
4035        assert_eq!(args.tag, "myfs");
4036        assert_eq!(args.path, "/path");
4037
4038        // Error: wrong number of fields
4039        assert!(FsArgs::from_str("myfs").is_err());
4040        assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
4041    }
4042
4043    #[test]
4044    fn test_fs_args_with_options_pcie_port() {
4045        // Without pcie_port
4046        let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
4047        assert_eq!(args.tag, "myfs");
4048        assert_eq!(args.path, "/path");
4049        assert_eq!(args.options, "uid=1000");
4050        assert!(args.pcie_port.is_none());
4051
4052        // With pcie_port
4053        let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
4054        assert_eq!(args.pcie_port.unwrap(), "rp0");
4055        assert_eq!(args.tag, "myfs");
4056        assert_eq!(args.path, "/path");
4057        assert_eq!(args.options, "uid=1000");
4058
4059        // Error: missing path
4060        assert!(FsArgsWithOptions::from_str("myfs").is_err());
4061    }
4062
4063    #[test]
4064    fn test_virtio_pmem_args_pcie_port() {
4065        // Without pcie_port
4066        let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
4067        assert_eq!(args.path, "/path/to/file");
4068        assert!(args.pcie_port.is_none());
4069
4070        // With pcie_port
4071        let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
4072        assert_eq!(args.pcie_port.unwrap(), "rp0");
4073        assert_eq!(args.path, "/path/to/file");
4074
4075        // Error: empty path
4076        assert!(VirtioPmemArgs::from_str("").is_err());
4077        assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
4078    }
4079
4080    #[test]
4081    fn test_smt_config_from_str() {
4082        assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
4083        assert_eq!(
4084            SmtConfigCli::from_str("force").unwrap(),
4085            SmtConfigCli::Force
4086        );
4087        assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
4088
4089        // Test error cases
4090        assert!(SmtConfigCli::from_str("invalid").is_err());
4091        assert!(SmtConfigCli::from_str("").is_err());
4092    }
4093
4094    #[test]
4095    fn test_pcat_boot_order_from_str() {
4096        // Test single device
4097        let order = PcatBootOrderCli::from_str("optical").unwrap();
4098        assert_eq!(order.0[0], PcatBootDevice::Optical);
4099
4100        // Test multiple devices
4101        let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
4102        assert_eq!(order.0[0], PcatBootDevice::HardDrive);
4103        assert_eq!(order.0[1], PcatBootDevice::Network);
4104
4105        // Test error cases
4106        assert!(PcatBootOrderCli::from_str("invalid").is_err());
4107        assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); // duplicate device
4108    }
4109
4110    #[test]
4111    fn test_floppy_disk_from_str() {
4112        // Test basic disk
4113        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
4114        assert!(!disk.read_only);
4115        match disk.kind {
4116            DiskCliKind::File {
4117                path,
4118                create_with_len,
4119                ..
4120            } => {
4121                assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
4122                assert_eq!(create_with_len, None);
4123            }
4124            _ => panic!("Expected File variant"),
4125        }
4126
4127        // Test with read-only flag
4128        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
4129        assert!(disk.read_only);
4130
4131        // Test error cases
4132        assert!(FloppyDiskCli::from_str("").is_err());
4133        assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
4134    }
4135
4136    #[test]
4137    fn test_pcie_root_complex_from_str() {
4138        const ONE_MB: u64 = 1024 * 1024;
4139        const ONE_GB: u64 = 1024 * ONE_MB;
4140
4141        const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
4142        const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
4143        const DEFAULT_HDM: u64 = ONE_GB;
4144        const DEFAULT_HDM_WINDOW_RESTRICTIONS: CfmwsWindowRestrictions =
4145            CfmwsWindowRestrictions::DEVICE_COHERENT;
4146
4147        assert_eq!(
4148            PcieRootComplexCli::from_str("rc0").unwrap(),
4149            PcieRootComplexCli {
4150                name: "rc0".to_string(),
4151                segment: 0,
4152                start_bus: 0,
4153                end_bus: 255,
4154                low_mmio: DEFAULT_LOW_MMIO,
4155                high_mmio: DEFAULT_HIGH_MMIO,
4156                hdm: DEFAULT_HDM,
4157                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4158                vnode: None,
4159                low_mmio_base: None,
4160                high_mmio_base: None,
4161                preserve_bars: false,
4162            }
4163        );
4164
4165        assert_eq!(
4166            PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
4167            PcieRootComplexCli {
4168                name: "rc1".to_string(),
4169                segment: 1,
4170                start_bus: 0,
4171                end_bus: 255,
4172                low_mmio: DEFAULT_LOW_MMIO,
4173                high_mmio: DEFAULT_HIGH_MMIO,
4174                hdm: DEFAULT_HDM,
4175                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4176                vnode: None,
4177                low_mmio_base: None,
4178                high_mmio_base: None,
4179                preserve_bars: false,
4180            }
4181        );
4182
4183        assert_eq!(
4184            PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
4185            PcieRootComplexCli {
4186                name: "rc2".to_string(),
4187                segment: 0,
4188                start_bus: 32,
4189                end_bus: 255,
4190                low_mmio: DEFAULT_LOW_MMIO,
4191                high_mmio: DEFAULT_HIGH_MMIO,
4192                hdm: DEFAULT_HDM,
4193                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4194                vnode: None,
4195                low_mmio_base: None,
4196                high_mmio_base: None,
4197                preserve_bars: false,
4198            }
4199        );
4200
4201        assert_eq!(
4202            PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
4203            PcieRootComplexCli {
4204                name: "rc3".to_string(),
4205                segment: 0,
4206                start_bus: 0,
4207                end_bus: 31,
4208                low_mmio: DEFAULT_LOW_MMIO,
4209                high_mmio: DEFAULT_HIGH_MMIO,
4210                hdm: DEFAULT_HDM,
4211                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4212                vnode: None,
4213                low_mmio_base: None,
4214                high_mmio_base: None,
4215                preserve_bars: false,
4216            }
4217        );
4218
4219        assert_eq!(
4220            PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
4221            PcieRootComplexCli {
4222                name: "rc4".to_string(),
4223                segment: 0,
4224                start_bus: 32,
4225                end_bus: 127,
4226                low_mmio: DEFAULT_LOW_MMIO,
4227                high_mmio: 2 * ONE_GB,
4228                hdm: DEFAULT_HDM,
4229                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4230                vnode: None,
4231                low_mmio_base: None,
4232                high_mmio_base: None,
4233                preserve_bars: false,
4234            }
4235        );
4236
4237        assert_eq!(
4238            PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
4239            PcieRootComplexCli {
4240                name: "rc5".to_string(),
4241                segment: 2,
4242                start_bus: 32,
4243                end_bus: 127,
4244                low_mmio: DEFAULT_LOW_MMIO,
4245                high_mmio: DEFAULT_HIGH_MMIO,
4246                hdm: DEFAULT_HDM,
4247                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4248                vnode: None,
4249                low_mmio_base: None,
4250                high_mmio_base: None,
4251                preserve_bars: false,
4252            }
4253        );
4254
4255        assert_eq!(
4256            PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
4257            PcieRootComplexCli {
4258                name: "rc6".to_string(),
4259                segment: 0,
4260                start_bus: 0,
4261                end_bus: 255,
4262                low_mmio: ONE_MB as u32,
4263                high_mmio: 64 * ONE_GB,
4264                hdm: DEFAULT_HDM,
4265                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4266                vnode: None,
4267                low_mmio_base: None,
4268                high_mmio_base: None,
4269                preserve_bars: false,
4270            }
4271        );
4272
4273        assert_eq!(
4274            PcieRootComplexCli::from_str("rc7,hdm=2G").unwrap(),
4275            PcieRootComplexCli {
4276                name: "rc7".to_string(),
4277                segment: 0,
4278                start_bus: 0,
4279                end_bus: 255,
4280                low_mmio: DEFAULT_LOW_MMIO,
4281                high_mmio: DEFAULT_HIGH_MMIO,
4282                hdm: 2 * ONE_GB,
4283                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4284                vnode: None,
4285                low_mmio_base: None,
4286                high_mmio_base: None,
4287                preserve_bars: false,
4288            }
4289        );
4290
4291        assert_eq!(
4292            PcieRootComplexCli::from_str("rc8,hdm_window_restrictions=0x21").unwrap(),
4293            PcieRootComplexCli {
4294                name: "rc8".to_string(),
4295                segment: 0,
4296                start_bus: 0,
4297                end_bus: 255,
4298                low_mmio: DEFAULT_LOW_MMIO,
4299                high_mmio: DEFAULT_HIGH_MMIO,
4300                hdm: DEFAULT_HDM,
4301                hdm_window_restrictions: CfmwsWindowRestrictions::try_from_bits(0x21).unwrap(),
4302                vnode: None,
4303                low_mmio_base: None,
4304                high_mmio_base: None,
4305                preserve_bars: false,
4306            }
4307        );
4308
4309        // Error cases
4310        assert!(PcieRootComplexCli::from_str("").is_err());
4311        assert!(PcieRootComplexCli::from_str("poorly,").is_err());
4312        assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
4313        assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
4314        assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
4315        assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
4316        assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
4317        assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
4318        assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
4319        assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
4320        assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
4321        assert!(PcieRootComplexCli::from_str("rc,hdm=bad").is_err());
4322        assert!(PcieRootComplexCli::from_str("rc,hdm").is_err());
4323        assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions=bad").is_err());
4324        assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions").is_err());
4325        assert!(PcieRootComplexCli::from_str("rc,cxl").is_err());
4326
4327        // node option
4328        assert_eq!(
4329            PcieRootComplexCli::from_str("rc9,node=1").unwrap(),
4330            PcieRootComplexCli {
4331                name: "rc9".to_string(),
4332                segment: 0,
4333                start_bus: 0,
4334                end_bus: 255,
4335                low_mmio: DEFAULT_LOW_MMIO,
4336                high_mmio: DEFAULT_HIGH_MMIO,
4337                hdm: DEFAULT_HDM,
4338                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4339                vnode: Some(1),
4340                low_mmio_base: None,
4341                high_mmio_base: None,
4342                preserve_bars: false,
4343            }
4344        );
4345    }
4346
4347    #[test]
4348    fn test_pcie_root_port_from_str() {
4349        assert_eq!(
4350            PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
4351            PcieRootPortCli {
4352                root_complex_name: "rc0".to_string(),
4353                name: "rc0rp0".to_string(),
4354                devfn: None,
4355                hotplug: false,
4356                acs_capabilities_supported: None,
4357                cxl: false,
4358                pasid: false,
4359            }
4360        );
4361
4362        assert_eq!(
4363            PcieRootPortCli::from_str("my_rc:port2").unwrap(),
4364            PcieRootPortCli {
4365                root_complex_name: "my_rc".to_string(),
4366                name: "port2".to_string(),
4367                devfn: None,
4368                hotplug: false,
4369                acs_capabilities_supported: None,
4370                cxl: false,
4371                pasid: false,
4372            }
4373        );
4374
4375        // Test with hotplug flag
4376        assert_eq!(
4377            PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
4378            PcieRootPortCli {
4379                root_complex_name: "my_rc".to_string(),
4380                name: "port2".to_string(),
4381                devfn: None,
4382                hotplug: true,
4383                acs_capabilities_supported: None,
4384                cxl: false,
4385                pasid: false,
4386            }
4387        );
4388
4389        assert_eq!(
4390            PcieRootPortCli::from_str("my_rc:port3,acs=0").unwrap(),
4391            PcieRootPortCli {
4392                root_complex_name: "my_rc".to_string(),
4393                name: "port3".to_string(),
4394                devfn: None,
4395                hotplug: false,
4396                acs_capabilities_supported: Some(0),
4397                cxl: false,
4398                pasid: false,
4399            }
4400        );
4401
4402        assert_eq!(
4403            PcieRootPortCli::from_str("my_rc:port3,acs=0x5f").unwrap(),
4404            PcieRootPortCli {
4405                root_complex_name: "my_rc".to_string(),
4406                name: "port3".to_string(),
4407                devfn: None,
4408                hotplug: false,
4409                acs_capabilities_supported: Some(0x005f),
4410                cxl: false,
4411                pasid: false,
4412            }
4413        );
4414
4415        assert_eq!(
4416            PcieRootPortCli::from_str("my_rc:port4,cxl").unwrap(),
4417            PcieRootPortCli {
4418                root_complex_name: "my_rc".to_string(),
4419                name: "port4".to_string(),
4420                devfn: None,
4421                hotplug: false,
4422                acs_capabilities_supported: None,
4423                cxl: true,
4424                pasid: false,
4425            }
4426        );
4427
4428        // Test addr= (device only, and device.function)
4429        assert_eq!(
4430            PcieRootPortCli::from_str("my_rc:port5,addr=5").unwrap(),
4431            PcieRootPortCli {
4432                root_complex_name: "my_rc".to_string(),
4433                name: "port5".to_string(),
4434                devfn: Some(5 << 3),
4435                hotplug: false,
4436                acs_capabilities_supported: None,
4437                cxl: false,
4438                pasid: false,
4439            }
4440        );
4441        assert_eq!(
4442            PcieRootPortCli::from_str("my_rc:port6,addr=5.1").unwrap(),
4443            PcieRootPortCli {
4444                root_complex_name: "my_rc".to_string(),
4445                name: "port6".to_string(),
4446                devfn: Some((5 << 3) | 1),
4447                hotplug: false,
4448                acs_capabilities_supported: None,
4449                cxl: false,
4450                pasid: false,
4451            }
4452        );
4453        assert_eq!(
4454            PcieRootPortCli::from_str("my_rc:port7,addr=0x1f.7").unwrap(),
4455            PcieRootPortCli {
4456                root_complex_name: "my_rc".to_string(),
4457                name: "port7".to_string(),
4458                devfn: Some(0xff),
4459                hotplug: false,
4460                acs_capabilities_supported: None,
4461                cxl: false,
4462                pasid: false,
4463            }
4464        );
4465
4466        assert_eq!(
4467            PcieRootPortCli::from_str("my_rc:port8,pasid").unwrap(),
4468            PcieRootPortCli {
4469                root_complex_name: "my_rc".to_string(),
4470                name: "port8".to_string(),
4471                devfn: None,
4472                hotplug: false,
4473                acs_capabilities_supported: None,
4474                cxl: false,
4475                pasid: true,
4476            }
4477        );
4478
4479        // Error cases
4480        assert!(PcieRootPortCli::from_str("").is_err());
4481        assert!(PcieRootPortCli::from_str("rp0").is_err());
4482        assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
4483        assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
4484        assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
4485        assert!(PcieRootPortCli::from_str("rc0:rp0,cxl=true").is_err());
4486        assert!(PcieRootPortCli::from_str("rc0:rp0,addr=32").is_err());
4487        assert!(PcieRootPortCli::from_str("rc0:rp0,addr=0.8").is_err());
4488        assert!(PcieRootPortCli::from_str("rc0:rp0,addr=1.2.3").is_err());
4489        assert!(PcieRootPortCli::from_str("rc0:rp0,addr").is_err());
4490        assert!(PcieRootPortCli::from_str("rc0:rp0,pasid=foo").is_err());
4491    }
4492
4493    #[test]
4494    fn test_pcie_generic_initiator_from_str() {
4495        assert_eq!(
4496            PcieGenericInitiatorCli::from_str("port=rp0,node=1").unwrap(),
4497            PcieGenericInitiatorCli {
4498                port_name: "rp0".to_string(),
4499                node: 1,
4500            }
4501        );
4502
4503        // Order should not matter.
4504        assert_eq!(
4505            PcieGenericInitiatorCli::from_str("node=2,port=sw0-downstream-1").unwrap(),
4506            PcieGenericInitiatorCli {
4507                port_name: "sw0-downstream-1".to_string(),
4508                node: 2,
4509            }
4510        );
4511
4512        // Error cases
4513        assert!(PcieGenericInitiatorCli::from_str("").is_err());
4514        assert!(PcieGenericInitiatorCli::from_str("port=rp0").is_err());
4515        assert!(PcieGenericInitiatorCli::from_str("node=1").is_err());
4516        assert!(PcieGenericInitiatorCli::from_str("rp0=1").is_err());
4517        assert!(PcieGenericInitiatorCli::from_str("port=,node=1").is_err());
4518        assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=x").is_err());
4519        assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=1,extra").is_err());
4520    }
4521
4522    #[test]
4523    fn test_pcie_switch_from_str() {
4524        assert_eq!(
4525            GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
4526            GenericPcieSwitchCli {
4527                port_name: "rp0".to_string(),
4528                name: "switch0".to_string(),
4529                num_downstream_ports: 4,
4530                hotplug: false,
4531                acs_capabilities_supported: None,
4532                pasid: false,
4533            }
4534        );
4535
4536        assert_eq!(
4537            GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
4538            GenericPcieSwitchCli {
4539                port_name: "port1".to_string(),
4540                name: "my_switch".to_string(),
4541                num_downstream_ports: 4,
4542                hotplug: false,
4543                acs_capabilities_supported: None,
4544                pasid: false,
4545            }
4546        );
4547
4548        assert_eq!(
4549            GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
4550            GenericPcieSwitchCli {
4551                port_name: "rp2".to_string(),
4552                name: "sw".to_string(),
4553                num_downstream_ports: 8,
4554                hotplug: false,
4555                acs_capabilities_supported: None,
4556                pasid: false,
4557            }
4558        );
4559
4560        // Test hierarchical connections
4561        assert_eq!(
4562            GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
4563            GenericPcieSwitchCli {
4564                port_name: "switch0-downstream-1".to_string(),
4565                name: "child_switch".to_string(),
4566                num_downstream_ports: 4,
4567                hotplug: false,
4568                acs_capabilities_supported: None,
4569                pasid: false,
4570            }
4571        );
4572
4573        // Test hotplug flag
4574        assert_eq!(
4575            GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
4576            GenericPcieSwitchCli {
4577                port_name: "rp0".to_string(),
4578                name: "switch0".to_string(),
4579                num_downstream_ports: 4,
4580                hotplug: true,
4581                acs_capabilities_supported: None,
4582                pasid: false,
4583            }
4584        );
4585
4586        // Test hotplug with num_downstream_ports
4587        assert_eq!(
4588            GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
4589            GenericPcieSwitchCli {
4590                port_name: "rp0".to_string(),
4591                name: "switch0".to_string(),
4592                num_downstream_ports: 8,
4593                hotplug: true,
4594                acs_capabilities_supported: None,
4595                pasid: false,
4596            }
4597        );
4598
4599        assert_eq!(
4600            GenericPcieSwitchCli::from_str("rp0:switch0,acs=0").unwrap(),
4601            GenericPcieSwitchCli {
4602                port_name: "rp0".to_string(),
4603                name: "switch0".to_string(),
4604                num_downstream_ports: 4,
4605                hotplug: false,
4606                acs_capabilities_supported: Some(0),
4607                pasid: false,
4608            }
4609        );
4610
4611        assert_eq!(
4612            GenericPcieSwitchCli::from_str("rp0:switch0,acs=95").unwrap(),
4613            GenericPcieSwitchCli {
4614                port_name: "rp0".to_string(),
4615                name: "switch0".to_string(),
4616                num_downstream_ports: 4,
4617                hotplug: false,
4618                acs_capabilities_supported: Some(95),
4619                pasid: false,
4620            }
4621        );
4622
4623        assert_eq!(
4624            GenericPcieSwitchCli::from_str("rp0:switch0,pasid").unwrap(),
4625            GenericPcieSwitchCli {
4626                port_name: "rp0".to_string(),
4627                name: "switch0".to_string(),
4628                num_downstream_ports: 4,
4629                hotplug: false,
4630                acs_capabilities_supported: None,
4631                pasid: true,
4632            }
4633        );
4634
4635        // Error cases
4636        assert!(GenericPcieSwitchCli::from_str("").is_err());
4637        assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
4638        assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
4639        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
4640        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
4641        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
4642        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
4643        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,pasid=bar").is_err());
4644    }
4645
4646    #[test]
4647    fn test_pcie_remote_from_str() {
4648        // Basic port name only
4649        assert_eq!(
4650            PcieRemoteCli::from_str("rc0rp0").unwrap(),
4651            PcieRemoteCli {
4652                port_name: "rc0rp0".to_string(),
4653                socket_addr: None,
4654                hu: 0,
4655                controller: 0,
4656            }
4657        );
4658
4659        // With socket address
4660        assert_eq!(
4661            PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
4662            PcieRemoteCli {
4663                port_name: "rc0rp0".to_string(),
4664                socket_addr: Some("localhost:22567".to_string()),
4665                hu: 0,
4666                controller: 0,
4667            }
4668        );
4669
4670        // With all options
4671        assert_eq!(
4672            PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
4673            PcieRemoteCli {
4674                port_name: "myport".to_string(),
4675                socket_addr: Some("localhost:22568".to_string()),
4676                hu: 1,
4677                controller: 2,
4678            }
4679        );
4680
4681        // Only hu and controller
4682        assert_eq!(
4683            PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
4684            PcieRemoteCli {
4685                port_name: "port0".to_string(),
4686                socket_addr: None,
4687                hu: 5,
4688                controller: 3,
4689            }
4690        );
4691
4692        // Error cases
4693        assert!(PcieRemoteCli::from_str("").is_err());
4694        assert!(PcieRemoteCli::from_str("port,socket=").is_err());
4695        assert!(PcieRemoteCli::from_str("port,hu=").is_err());
4696        assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
4697        assert!(PcieRemoteCli::from_str("port,controller=").is_err());
4698        assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
4699        assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
4700    }
4701
4702    #[test]
4703    fn test_parse_memory_units() {
4704        assert_eq!(parse_memory("64G").unwrap(), 64 * 1024 * 1024 * 1024);
4705        assert_eq!(parse_memory("64GB").unwrap(), 64 * 1024 * 1024 * 1024);
4706        assert_eq!(parse_memory("3MB").unwrap(), 3 * 1024 * 1024);
4707        assert_eq!(parse_memory("512KB").unwrap(), 512 * 1024);
4708        assert!(parse_memory("3MiB").is_err());
4709    }
4710
4711    #[test]
4712    fn test_memory_config_size_only() {
4713        assert_eq!(
4714            parse_memory_config("64G").unwrap(),
4715            MemoryCli {
4716                size: Some(vmm_cli::MemorySize(64 * 1024 * 1024 * 1024)),
4717                ..Default::default()
4718            }
4719        );
4720    }
4721
4722    #[test]
4723    fn test_memory_config_key_value() {
4724        assert_eq!(
4725            parse_memory_config("size=2G,shared=off,prefetch=on,thp=on").unwrap(),
4726            MemoryCli {
4727                size: Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024)),
4728                shared: Some(false),
4729                prefetch: true,
4730                transparent_hugepages: Some(true),
4731                ..Default::default()
4732            }
4733        );
4734
4735        assert_eq!(
4736            parse_memory_config("size=4GB,hugepages=on,hugepage_size=2MB").unwrap(),
4737            MemoryCli {
4738                size: Some(vmm_cli::MemorySize(4 * 1024 * 1024 * 1024)),
4739                hugepages: true,
4740                hugepage_size: Some(vmm_cli::MemorySize(2 * 1024 * 1024)),
4741                ..Default::default()
4742            }
4743        );
4744
4745        assert_eq!(
4746            parse_memory_config("file=/tmp/memory.bin").unwrap(),
4747            MemoryCli {
4748                file: Some(PathBuf::from("/tmp/memory.bin")),
4749                ..Default::default()
4750            }
4751        );
4752    }
4753
4754    #[test]
4755    fn test_memory_config_rejects_invalid_combinations() {
4756        assert!(parse_memory_config("size=1G,size=2G").is_err());
4757        assert!(parse_memory_config("hugepage_size=2M").is_err());
4758        assert!(parse_memory_config("hugepages=on,shared=off").is_err());
4759        assert!(parse_memory_config("hugepages=on,file=/tmp/memory.bin").is_err());
4760
4761        // Semantic validation of the hugepage size happens in the memory
4762        // builder, not in CLI parsing.
4763        assert_eq!(
4764            parse_memory_config("hugepages=on,hugepage_size=3MB")
4765                .unwrap()
4766                .hugepage_size,
4767            Some(vmm_cli::MemorySize(3 * 1024 * 1024))
4768        );
4769    }
4770
4771    #[test]
4772    fn test_memory_options_merge_legacy_aliases() {
4773        let opt = Options::try_parse_from([
4774            "openvmm",
4775            "--memory",
4776            "2G",
4777            "--prefetch",
4778            "--private-memory",
4779            "--thp",
4780        ])
4781        .unwrap();
4782        opt.validate_memory_options().unwrap();
4783        assert_eq!(opt.memory_size(), 2 * 1024 * 1024 * 1024);
4784        assert!(opt.prefetch_memory());
4785        assert!(opt.private_memory());
4786        assert!(opt.transparent_hugepages());
4787    }
4788
4789    #[test]
4790    fn test_serial_debugger_mode_option_parsed() {
4791        // No COM port configured: no debugger mode.
4792        let opt = Options::try_parse_from(["openvmm"]).unwrap();
4793        assert!(opt.com1.is_none());
4794
4795        // A plain backend is not in debugger mode.
4796        let opt = Options::try_parse_from(["openvmm", "--com1", "none"]).unwrap();
4797        let com1 = opt.com1.unwrap();
4798        assert!(!com1.debugger_mode);
4799        assert_eq!(com1.backend, SerialConfigCli::None);
4800
4801        // The `debugger-mode:` prefix enables debugger mode for just that port,
4802        // and the remainder still parses as the backend.
4803        let opt = Options::try_parse_from([
4804            "openvmm",
4805            "--com1",
4806            "debugger-mode:listen=/tmp/kd",
4807            "--com2",
4808            "none",
4809        ])
4810        .unwrap();
4811        let com1 = opt.com1.unwrap();
4812        assert!(com1.debugger_mode);
4813        assert_eq!(com1.backend, SerialConfigCli::Pipe("/tmp/kd".into()));
4814        // Other ports remain independent (not in debugger mode).
4815        assert!(!opt.com2.unwrap().debugger_mode);
4816
4817        // The prefix must not eat colons in the backend (e.g. a tcp address).
4818        let opt = Options::try_parse_from([
4819            "openvmm",
4820            "--com1",
4821            "debugger-mode:listen=tcp:127.0.0.1:5555",
4822        ])
4823        .unwrap();
4824        let com1 = opt.com1.unwrap();
4825        assert!(com1.debugger_mode);
4826        assert_eq!(
4827            com1.backend,
4828            SerialConfigCli::Tcp("127.0.0.1:5555".parse().unwrap())
4829        );
4830    }
4831
4832    #[test]
4833    fn test_memory_options_allow_legacy_thp_with_new_private_memory() {
4834        let opt = Options::try_parse_from(["openvmm", "--memory", "shared=off", "--thp"]).unwrap();
4835        opt.validate_memory_options().unwrap();
4836        assert!(opt.private_memory());
4837        assert!(opt.transparent_hugepages());
4838    }
4839
4840    #[test]
4841    fn test_memory_options_reject_conflicting_legacy_aliases() {
4842        let opt = Options::try_parse_from(["openvmm", "--memory", "shared=on", "--private-memory"])
4843            .unwrap();
4844        assert!(opt.validate_memory_options().is_err());
4845    }
4846
4847    #[test]
4848    fn test_pidfile_option_parsed() {
4849        let opt = Options::try_parse_from(["openvmm", "--pidfile", "/tmp/test.pid"]).unwrap();
4850        assert_eq!(opt.pidfile, Some(PathBuf::from("/tmp/test.pid")));
4851    }
4852
4853    #[test]
4854    fn test_guest_power_action_flags() {
4855        // Defaults preserve the historical behavior: reset and watchdog reboot,
4856        // power-off and crash keep the stopped VM.
4857        let opt = Options::try_parse_from(["openvmm"]).unwrap();
4858        assert_eq!(opt.guest_reset_action, GuestPowerAction::Reset);
4859        assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Halt);
4860        assert_eq!(opt.guest_crash_action, GuestPowerAction::Halt);
4861        assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Reset);
4862        // The CLI defaults must match the shared GuestPowerActions::default() the
4863        // ttrpc server uses, so the two launch paths never drift.
4864        assert_eq!(
4865            crate::vm_controller::GuestPowerActions {
4866                shutdown: opt.guest_shutdown_action,
4867                reset: opt.guest_reset_action,
4868                crash: opt.guest_crash_action,
4869                watchdog: opt.guest_watchdog_action,
4870            },
4871            crate::vm_controller::GuestPowerActions::default(),
4872        );
4873
4874        let opt = Options::try_parse_from([
4875            "openvmm",
4876            "--guest-watchdog",
4877            "--guest-reset-action",
4878            "exit",
4879            "--guest-shutdown-action",
4880            "exit:5",
4881            "--guest-crash-action",
4882            "reset",
4883            "--guest-watchdog-action",
4884            "halt",
4885        ])
4886        .unwrap();
4887        // A bare `exit` is status 0; `exit:5` carries the code through.
4888        assert_eq!(opt.guest_reset_action, GuestPowerAction::Exit(0));
4889        assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Exit(5));
4890        assert_eq!(opt.guest_crash_action, GuestPowerAction::Reset);
4891        assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Halt);
4892
4893        // Malformed and out-of-range exit codes are rejected (status is 0-255).
4894        assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:nope"]).is_err());
4895        assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:300"]).is_err());
4896        assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:-1"]).is_err());
4897
4898        // --guest-watchdog-action requires the watchdog device (--guest-watchdog).
4899        assert!(Options::try_parse_from(["openvmm", "--guest-watchdog-action", "halt"]).is_err());
4900    }
4901
4902    #[cfg(target_os = "linux")]
4903    #[test]
4904    fn test_vfio_device_cli_parse() {
4905        use vfio_assigned_device_resources::BarAddressConfig;
4906
4907        // Required keys only.
4908        let v = VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0").unwrap();
4909        assert_eq!(v.pci_id, "0000:01:00.0");
4910        assert_eq!(v.port_name, "rp0");
4911        assert_eq!(v.iommu, None);
4912
4913        // With optional iommu= key. Keys may appear in any order.
4914        let v = VfioDeviceCli::from_str("port=rp1,iommu=iommu0,host=0000:02:00.0").unwrap();
4915        assert_eq!(v.pci_id, "0000:02:00.0");
4916        assert_eq!(v.port_name, "rp1");
4917        assert_eq!(v.iommu.as_deref(), Some("iommu0"));
4918
4919        let v = VfioDeviceCli::from_str(
4920            "host=0000:03:00.0,port=rp2,bar0=host,bar2=0x80000000,bar4=0x110000000000",
4921        )
4922        .unwrap();
4923        assert_eq!(v.bar_addresses[0], BarAddressConfig::HostAssigned);
4924        assert_eq!(v.bar_addresses[1], BarAddressConfig::GuestAssigned);
4925        assert_eq!(v.bar_addresses[2], BarAddressConfig::Fixed(0x80000000));
4926        assert_eq!(v.bar_addresses[4], BarAddressConfig::Fixed(0x110000000000));
4927    }
4928
4929    #[cfg(target_os = "linux")]
4930    #[test]
4931    fn test_vfio_device_cli_errors() {
4932        // Missing required keys.
4933        assert!(VfioDeviceCli::from_str("port=rp0").is_err());
4934        assert!(VfioDeviceCli::from_str("host=0000:01:00.0").is_err());
4935
4936        // Unknown key.
4937        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,foo=bar").is_err());
4938
4939        // Duplicate keys are rejected.
4940        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,host=0000:02:00.0,port=rp0").is_err());
4941        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,port=rp1").is_err());
4942        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=a,iommu=b").is_err());
4943
4944        // Empty values are rejected.
4945        assert!(VfioDeviceCli::from_str("host=,port=rp0").is_err());
4946        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=").is_err());
4947        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=").is_err());
4948
4949        // Missing '=' separator.
4950        assert!(VfioDeviceCli::from_str("host").is_err());
4951        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu").is_err());
4952
4953        // Path-traversal characters in the host BDF are rejected.
4954        assert!(VfioDeviceCli::from_str("host=../../etc/passwd,port=rp0").is_err());
4955        assert!(VfioDeviceCli::from_str("host=foo/bar,port=rp0").is_err());
4956
4957        // Invalid and duplicate BAR configurations are rejected.
4958        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0").is_err());
4959        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x0").is_err());
4960        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0xnope").is_err());
4961        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=pt").is_err());
4962        assert!(
4963            VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x1000,bar0=host").is_err()
4964        );
4965    }
4966
4967    #[cfg(target_os = "linux")]
4968    #[test]
4969    fn test_iommu_cli_parse() {
4970        let c = IommuCli::from_str("id=iommu0").unwrap();
4971        assert_eq!(c.id, "iommu0");
4972
4973        // Wrong key.
4974        assert!(IommuCli::from_str("name=iommu0").is_err());
4975
4976        // Missing '=' separator.
4977        assert!(IommuCli::from_str("iommu0").is_err());
4978
4979        // Empty id.
4980        assert!(IommuCli::from_str("id=").is_err());
4981    }
4982
4983    #[cfg(target_os = "linux")]
4984    #[test]
4985    fn test_vhost_user_cli() {
4986        // type=blk: socket is positional; num_queues/queue_size optional.
4987        let v = VhostUserCli::from_str("/run/blk.sock,type=blk,num_queues=2").unwrap();
4988        assert_eq!(v.socket_path, "/run/blk.sock");
4989        assert!(matches!(
4990            v.device_type,
4991            VhostUserDeviceTypeCli::Blk {
4992                num_queues: Some(2),
4993                queue_size: None
4994            }
4995        ));
4996        assert_eq!(v.pcie_port, None);
4997
4998        // type=fs requires a tag.
4999        let v = VhostUserCli::from_str("/run/fs.sock,type=fs,tag=myfs,pcie_port=p0").unwrap();
5000        assert!(matches!(
5001            &v.device_type,
5002            VhostUserDeviceTypeCli::Fs { tag, .. } if tag == "myfs"
5003        ));
5004        assert_eq!(v.pcie_port.as_deref(), Some("p0"));
5005
5006        // device_id with a bracketed queue_sizes list.
5007        let v = VhostUserCli::from_str("/run/x.sock,device_id=9,queue_sizes=[16,32]").unwrap();
5008        assert!(matches!(
5009            &v.device_type,
5010            VhostUserDeviceTypeCli::Other { device_id: 9, queue_sizes } if *queue_sizes == vec![16, 32]
5011        ));
5012
5013        // Errors.
5014        assert!(VhostUserCli::from_str("/run/x.sock").is_err()); // neither type nor device_id
5015        assert!(VhostUserCli::from_str("/run/x.sock,type=blk,device_id=1").is_err()); // both
5016        assert!(VhostUserCli::from_str("/run/x.sock,type=fs").is_err()); // fs without tag
5017        assert!(VhostUserCli::from_str("/run/x.sock,type=zzz").is_err()); // unknown type
5018        assert!(VhostUserCli::from_str("/run/x.sock,type=blk,tag=t").is_err()); // tag on non-fs
5019        assert!(VhostUserCli::from_str("/run/x.sock,type=blk,queue_sizes=[1]").is_err()); // queue_sizes on non-device_id
5020        assert!(VhostUserCli::from_str("/run/x.sock,device_id=1,num_queues=2").is_err()); // num_queues on device_id
5021        assert!(VhostUserCli::from_str("/run/x.sock,device_id=1,queue_sizes=[]").is_err()); // empty list
5022        assert!(VhostUserCli::from_str("/run/x.sock,device_id=1").is_err()); // device_id without queue_sizes
5023    }
5024
5025    #[cfg(target_os = "linux")]
5026    #[test]
5027    fn test_vhost_vsock_cli() {
5028        let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "3"]).unwrap();
5029        assert_eq!(opt.virtio_vsock_vhost_cid, Some(3));
5030
5031        assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "2"]).is_err());
5032        assert!(
5033            Options::try_parse_from([
5034                "openvmm",
5035                "--virtio-vsock-vhost-cid",
5036                "3",
5037                "--virtio-vsock-path",
5038                "/tmp/vsock",
5039            ])
5040            .is_err()
5041        );
5042    }
5043
5044    #[test]
5045    fn test_nvme_controller_cli_pcie() {
5046        let c = NvmeControllerCli::from_str("id=nvme0,pcie_port=p0").unwrap();
5047        assert_eq!(c.id, "nvme0");
5048        assert_eq!(c.transport, NvmeControllerTransport::Pcie("p0".into()));
5049    }
5050
5051    #[test]
5052    fn test_nvme_controller_cli_vpci_no_guid() {
5053        let c = NvmeControllerCli::from_str("id=nvme1,vpci").unwrap();
5054        assert_eq!(c.id, "nvme1");
5055        assert!(matches!(c.transport, NvmeControllerTransport::Vpci(None)));
5056        assert!(NvmeControllerCli::from_str("id=nvme1,vpci=").is_err());
5057    }
5058
5059    #[test]
5060    fn test_nvme_controller_cli_vpci_with_guid() {
5061        let c = NvmeControllerCli::from_str("id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c")
5062            .unwrap();
5063        assert_eq!(c.id, "nvme2");
5064        assert!(matches!(
5065            c.transport,
5066            NvmeControllerTransport::Vpci(Some(_))
5067        ));
5068    }
5069
5070    #[test]
5071    fn test_nvme_controller_cli_errors() {
5072        // Missing id.
5073        assert!(NvmeControllerCli::from_str("pcie_port=p0").is_err());
5074        // Missing transport.
5075        assert!(NvmeControllerCli::from_str("id=nvme0").is_err());
5076        // Both transports.
5077        assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,vpci").is_err());
5078        // Unknown option.
5079        assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,foo=bar").is_err());
5080        // Empty id.
5081        assert!(NvmeControllerCli::from_str("id=,pcie_port=p0").is_err());
5082        // Empty pcie_port.
5083        assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=").is_err());
5084        // Invalid GUID.
5085        let err = NvmeControllerCli::from_str("id=nvme0,vpci=not-a-guid").unwrap_err();
5086        assert!(err.to_string().contains("invalid value for option 'vpci'"));
5087    }
5088
5089    #[test]
5090    fn test_disk_cli_controller() {
5091        let d = DiskCli::from_str("file:disk.vhd,on=nvme0").unwrap();
5092        assert_eq!(d.controller.as_deref(), Some("nvme0"));
5093        assert_eq!(d.nsid, None);
5094    }
5095
5096    #[test]
5097    fn test_disk_cli_controller_with_nsid() {
5098        let d = DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=3").unwrap();
5099        assert_eq!(d.controller.as_deref(), Some("nvme0"));
5100        assert_eq!(d.nsid, Some(3));
5101    }
5102
5103    #[test]
5104    fn test_disk_cli_controller_errors() {
5105        // nsid without on.
5106        assert!(DiskCli::from_str("file:disk.vhd,nsid=1").is_err());
5107        // lun without on.
5108        assert!(DiskCli::from_str("file:disk.vhd,lun=0").is_err());
5109        // on with pcie_port.
5110        assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,pcie_port=p0").is_err());
5111        // Empty controller name.
5112        assert!(DiskCli::from_str("file:disk.vhd,on=").is_err());
5113        // Invalid nsid.
5114        assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=abc").is_err());
5115        // nsid and lun together.
5116        assert!(DiskCli::from_str("file:disk.vhd,on=c,nsid=1,lun=0").is_err());
5117    }
5118
5119    #[test]
5120    fn test_disk_cli_controller_with_lun() {
5121        let d = DiskCli::from_str("file:disk.vhd,on=scsi0,lun=3").unwrap();
5122        assert_eq!(d.controller.as_deref(), Some("scsi0"));
5123        assert_eq!(d.lun, Some(3));
5124        assert_eq!(d.nsid, None);
5125    }
5126
5127    #[test]
5128    fn test_scsi_controller_cli() {
5129        let c = ScsiControllerCli::from_str("id=scsi0").unwrap();
5130        assert_eq!(c.id, "scsi0");
5131        assert_eq!(c.sub_channels, 0);
5132    }
5133
5134    #[test]
5135    fn test_scsi_controller_cli_with_sub_channels() {
5136        let c = ScsiControllerCli::from_str("id=scsi1,sub_channels=4").unwrap();
5137        assert_eq!(c.id, "scsi1");
5138        assert_eq!(c.sub_channels, 4);
5139    }
5140
5141    #[test]
5142    fn test_scsi_controller_cli_errors() {
5143        // Missing id.
5144        assert!(ScsiControllerCli::from_str("sub_channels=4").is_err());
5145        // Empty id.
5146        assert!(ScsiControllerCli::from_str("id=").is_err());
5147        // Unknown option.
5148        assert!(ScsiControllerCli::from_str("id=scsi0,foo=bar").is_err());
5149        // Invalid sub_channels.
5150        assert!(ScsiControllerCli::from_str("id=scsi0,sub_channels=abc").is_err());
5151    }
5152
5153    #[test]
5154    fn test_disk_cli_relay() {
5155        let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt").unwrap();
5156        assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
5157        assert_eq!(d.relay.as_ref().unwrap().1, None);
5158    }
5159
5160    #[test]
5161    fn test_disk_cli_relay_with_location() {
5162        let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:3").unwrap();
5163        assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
5164        assert_eq!(d.relay.as_ref().unwrap().1, Some(3));
5165    }
5166
5167    #[test]
5168    fn test_disk_cli_relay_errors() {
5169        // relay without on.
5170        assert!(DiskCli::from_str("file:disk.vhd,relay=tgt").is_err());
5171        // relay with uh.
5172        assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt,uh").is_err());
5173        // relay with invalid location.
5174        assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:abc").is_err());
5175        // empty relay.
5176        assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=").is_err());
5177    }
5178
5179    #[test]
5180    fn test_nvme_controller_cli_vtl2() {
5181        let c = NvmeControllerCli::from_str("id=nvme0,vpci,vtl2").unwrap();
5182        assert_eq!(c.vtl, DeviceVtl::Vtl2);
5183    }
5184
5185    #[test]
5186    fn test_scsi_controller_cli_vtl2() {
5187        let c = ScsiControllerCli::from_str("id=scsi0,vtl2").unwrap();
5188        assert_eq!(c.vtl, DeviceVtl::Vtl2);
5189    }
5190
5191    #[test]
5192    fn test_openhcl_controller_cli() {
5193        let c = OpenhclControllerCli::from_str("id=vtl0-scsi,type=scsi").unwrap();
5194        assert_eq!(c.id, "vtl0-scsi");
5195        assert_eq!(c.controller_type, OpenhclControllerType::Scsi);
5196        assert_eq!(c.guid, None);
5197    }
5198
5199    #[test]
5200    fn test_openhcl_controller_cli_nvme_with_guid() {
5201        let c = OpenhclControllerCli::from_str(
5202            "id=vtl0-nvme,type=nvme,guid=09a59b81-2bf6-4164-81d7-3a0dc977ba65",
5203        )
5204        .unwrap();
5205        assert_eq!(c.controller_type, OpenhclControllerType::Nvme);
5206        assert!(c.guid.is_some());
5207    }
5208
5209    #[test]
5210    fn test_openhcl_controller_cli_errors() {
5211        // Missing id.
5212        assert!(OpenhclControllerCli::from_str("type=scsi").is_err());
5213        // Missing type.
5214        assert!(OpenhclControllerCli::from_str("id=foo").is_err());
5215        // Invalid type.
5216        assert!(OpenhclControllerCli::from_str("id=foo,type=ide").is_err());
5217        // Invalid guid.
5218        assert!(OpenhclControllerCli::from_str("id=foo,type=scsi,guid=bad").is_err());
5219    }
5220
5221    #[test]
5222    fn test_parse_vp_list() {
5223        use vmm_cli::BracketRangeList;
5224
5225        let parse = |s: &str| {
5226            s.parse::<BracketRangeList>()
5227                .and_then(|v| v.expand_below(1024))
5228        };
5229
5230        // Individual indices.
5231        assert_eq!(parse("[0,1,2,3]").unwrap(), vec![0, 1, 2, 3]);
5232
5233        // Single index.
5234        assert_eq!(parse("[5]").unwrap(), vec![5]);
5235
5236        // Dash range.
5237        assert_eq!(parse("[0-3]").unwrap(), vec![0, 1, 2, 3]);
5238
5239        // Mixed indices and ranges.
5240        assert_eq!(parse("[0,1,4-6,10]").unwrap(), vec![0, 1, 4, 5, 6, 10]);
5241
5242        // Whitespace tolerance.
5243        assert_eq!(parse("[0, 1, 2-4]").unwrap(), vec![0, 1, 2, 3, 4]);
5244
5245        // Missing brackets.
5246        assert!(parse("0,1,2").is_err());
5247        assert!(parse("0-3").is_err());
5248
5249        // Inverted range.
5250        assert!(parse("[3-0]").is_err());
5251
5252        // Non-numeric.
5253        assert!(parse("[a,b]").is_err());
5254    }
5255
5256    #[test]
5257    fn test_parse_numa_node() {
5258        use super::parse_numa_node;
5259
5260        // Basic node with size only.
5261        let n = parse_numa_node("size=2G").unwrap();
5262        assert_eq!(
5263            n.memory.size,
5264            Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024))
5265        );
5266        assert!(n.vps.is_none());
5267        assert!(n.host_numa_node.is_none());
5268
5269        // Node with bracket VP list.
5270        let n = parse_numa_node("size=1G,vps=[0,1,2,3]").unwrap();
5271        assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
5272
5273        // Node with VP range in brackets.
5274        let n = parse_numa_node("size=1G,vps=[0-3]").unwrap();
5275        assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
5276
5277        // Node with host_numa_node.
5278        let n = parse_numa_node("size=1G,host_numa_node=1").unwrap();
5279        assert_eq!(n.host_numa_node, Some(1));
5280
5281        // All options together.
5282        let n = parse_numa_node("size=1G,vps=[0,1],host_numa_node=0,hugepages=on").unwrap();
5283        assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1]);
5284        assert_eq!(n.host_numa_node, Some(0));
5285        assert!(n.memory.hugepages);
5286
5287        // Missing size.
5288        assert!(parse_numa_node("vps=[0,1]").is_err());
5289
5290        // Bare vps without brackets.
5291        assert!(parse_numa_node("size=1G,vps=0,1").is_err());
5292
5293        // Duplicate vps.
5294        assert!(parse_numa_node("size=1G,vps=[0],vps=[1]").is_err());
5295
5296        // `file` is rejected for NUMA nodes.
5297        assert!(parse_numa_node("size=1G,file=/tmp/x").is_err());
5298
5299        // Empty vps=[] for memory-only node.
5300        let n = parse_numa_node("size=1G,vps=[]").unwrap();
5301        assert!(n.vps.unwrap().0.is_empty());
5302    }
5303
5304    #[test]
5305    fn test_parse_numa_distance() {
5306        use super::parse_numa_distance;
5307
5308        let d = parse_numa_distance("0:1:20").unwrap();
5309        assert_eq!(d.src, 0);
5310        assert_eq!(d.dst, 1);
5311        assert_eq!(d.distance, 20);
5312
5313        // Self-distance.
5314        let d = parse_numa_distance("0:0:10").unwrap();
5315        assert_eq!(d.distance, 10);
5316
5317        // Distance below minimum.
5318        assert!(parse_numa_distance("0:1:5").is_err());
5319
5320        // Wrong format.
5321        assert!(parse_numa_distance("0:1").is_err());
5322        assert!(parse_numa_distance("0:1:20:extra").is_err());
5323    }
5324
5325    #[cfg(guest_arch = "aarch64")]
5326    #[test]
5327    fn test_smmu_cli_from_str() {
5328        // Minimal: only rc=, oas defaults to auto, accel off.
5329        let s = SmmuCli::from_str("rc=pcie0").unwrap();
5330        assert_eq!(s.rc_name, "pcie0");
5331        assert!(!s.accel);
5332        assert!(matches!(s.oas, SmmuOasCli::Auto));
5333
5334        // accel flag.
5335        let s = SmmuCli::from_str("rc=pcie0,accel").unwrap();
5336        assert_eq!(s.rc_name, "pcie0");
5337        assert!(s.accel);
5338        assert!(matches!(s.oas, SmmuOasCli::Auto));
5339
5340        // Explicit oas=auto.
5341        let s = SmmuCli::from_str("rc=pcie0,oas=auto").unwrap();
5342        assert!(matches!(s.oas, SmmuOasCli::Auto));
5343
5344        // Fixed oas.
5345        let s = SmmuCli::from_str("rc=pcie0,oas=52").unwrap();
5346        assert!(matches!(s.oas, SmmuOasCli::Fixed(52)));
5347
5348        // All keys/flags together, order independent.
5349        let s = SmmuCli::from_str("oas=48,accel,rc=pcie1").unwrap();
5350        assert_eq!(s.rc_name, "pcie1");
5351        assert!(s.accel);
5352        assert!(matches!(s.oas, SmmuOasCli::Fixed(48)));
5353
5354        // Missing required rc=.
5355        assert!(SmmuCli::from_str("accel").is_err());
5356        assert!(SmmuCli::from_str("oas=52").is_err());
5357
5358        // Empty rc= value.
5359        assert!(SmmuCli::from_str("rc=").is_err());
5360
5361        // Duplicate rc= key.
5362        assert!(SmmuCli::from_str("rc=pcie0,rc=pcie1").is_err());
5363
5364        // Non-numeric oas value.
5365        assert!(SmmuCli::from_str("rc=pcie0,oas=big").is_err());
5366
5367        // Unknown key.
5368        assert!(SmmuCli::from_str("rc=pcie0,foo=bar").is_err());
5369
5370        // Unknown flag.
5371        assert!(SmmuCli::from_str("rc=pcie0,turbo").is_err());
5372    }
5373}