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 openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
25use openvmm_defs::config::DeviceVtl;
26use openvmm_defs::config::PcatBootDevice;
27use openvmm_defs::config::Vtl2BaseAddressType;
28use openvmm_defs::config::X2ApicConfig;
29use std::ffi::OsString;
30use std::net::SocketAddr;
31use std::path::PathBuf;
32use std::str::FromStr;
33use thiserror::Error;
34
35/// OpenVMM virtual machine monitor.
36///
37/// This is not yet a stable interface and may change radically between
38/// versions.
39#[derive(Parser)]
40pub struct Options {
41    /// processor count
42    #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
43    pub processors: u32,
44
45    /// guest RAM size
46    #[clap(
47        short = 'm',
48        long,
49        value_name = "SIZE",
50        default_value = "1GB",
51        value_parser = parse_memory
52    )]
53    pub memory: u64,
54
55    /// use shared memory segment
56    #[clap(short = 'M', long)]
57    pub shared_memory: bool,
58
59    /// prefetch guest RAM
60    #[clap(long)]
61    pub prefetch: bool,
62
63    /// back guest RAM with a file instead of anonymous memory.
64    /// The file is created/opened and sized to the guest RAM size.
65    /// Enables snapshot save (fsync) and restore (open + mmap).
66    #[clap(long, value_name = "FILE", conflicts_with = "private_memory")]
67    pub memory_backing_file: Option<PathBuf>,
68
69    /// Restore VM from a snapshot directory (implies file-backed memory from
70    /// the snapshot's memory.bin). Cannot be used with --memory-backing-file.
71    #[clap(long, value_name = "DIR", conflicts_with = "memory_backing_file")]
72    pub restore_snapshot: Option<PathBuf>,
73
74    /// use private anonymous memory for guest RAM
75    #[clap(long, conflicts_with_all = ["memory_backing_file", "restore_snapshot"])]
76    pub private_memory: bool,
77
78    /// enable transparent huge pages for guest RAM (Linux only, requires --private-memory)
79    #[clap(long, requires("private_memory"))]
80    pub thp: bool,
81
82    /// start in paused state
83    #[clap(short = 'P', long)]
84    pub paused: bool,
85
86    /// kernel image (when using linux direct boot)
87    #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
88    pub kernel: OptionalPathBuf,
89
90    /// initrd image (when using linux direct boot)
91    #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
92    pub initrd: OptionalPathBuf,
93
94    /// extra kernel command line args
95    #[clap(short = 'c', long, value_name = "STRING")]
96    pub cmdline: Vec<String>,
97
98    /// enable HV#1 capabilities
99    #[clap(long)]
100    pub hv: bool,
101
102    /// Use a full device tree instead of ACPI tables for ARM64 Linux direct
103    /// boot. By default, ARM64 uses ACPI mode (stub DT + EFI + ACPI tables).
104    /// This flag selects the legacy DT-only path. Rejected on x86.
105    #[clap(long, conflicts_with_all = ["uefi", "pcat", "igvm"])]
106    pub device_tree: bool,
107
108    /// enable vtl2 - only supported in WHP and simulated without hypervisor support currently
109    ///
110    /// Currently implies --get.
111    #[clap(long, requires("hv"))]
112    pub vtl2: bool,
113
114    /// Add GET and related devices for using the OpenHCL paravisor to the
115    /// highest enabled VTL.
116    #[clap(long, requires("hv"))]
117    pub get: bool,
118
119    /// Disable GET and related devices for using the OpenHCL paravisor, even
120    /// when --vtl2 is passed.
121    #[clap(long, conflicts_with("get"))]
122    pub no_get: bool,
123
124    /// disable the VTL0 alias map presented to VTL2 by default
125    #[clap(long, requires("vtl2"))]
126    pub no_alias_map: bool,
127
128    /// enable isolation emulation
129    #[clap(long, requires("vtl2"))]
130    pub isolation: Option<IsolationCli>,
131
132    /// the hybrid vsock listener path
133    #[clap(long, value_name = "PATH", alias = "vsock-path")]
134    pub vmbus_vsock_path: Option<String>,
135
136    /// the VTL2 hybrid vsock listener path
137    #[clap(long, value_name = "PATH", requires("vtl2"), alias = "vtl2-vsock-path")]
138    pub vmbus_vtl2_vsock_path: Option<String>,
139
140    /// the late map vtl0 ram access policy when vtl2 is enabled
141    #[clap(long, requires("vtl2"), default_value = "halt")]
142    pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
143
144    /// attach a disk (can be passed multiple times)
145    #[clap(long_help = r#"
146e.g: --disk memdiff:file:/path/to/disk.vhd
147
148syntax: <path> | kind:<arg>[,flag,opt=arg,...]
149
150valid disk kinds:
151    `mem:<len>`                    memory backed disk
152        <len>: length of ramdisk, e.g.: `1G`
153    `memdiff:<disk>`               memory backed diff disk
154        <disk>: lower disk, e.g.: `file:base.img`
155    `file:<path>[;direct][;create=<len>]`   file-backed disk
156        <path>: path to file
157        `;direct`: bypass the OS page cache
158    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
159    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
160    `autocache:<key>:<disk>`       auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
161    `blob:<type>:<url>`            HTTP blob (read-only)
162        <type>: `flat` or `vhd1`
163    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
164        <cipher>: `xts-aes-256`
165    `prwrap:<disk>`                persistent reservations wrapper
166
167flags:
168    `ro`                           open disk as read-only
169    `dvd`                          specifies that device is cd/dvd and it is read_only
170    `vtl2`                         assign this disk to VTL2
171    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
172    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
173
174options:
175    `pcie_port=<name>`             present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
176"#)]
177    #[clap(long, value_name = "FILE")]
178    pub disk: Vec<DiskCli>,
179
180    /// attach a disk via an NVMe controller
181    #[clap(long_help = r#"
182e.g: --nvme memdiff:file:/path/to/disk.vhd
183
184syntax: <path> | kind:<arg>[,flag,opt=arg,...]
185
186valid disk kinds:
187    `mem:<len>`                    memory backed disk
188        <len>: length of ramdisk, e.g.: `1G`
189    `memdiff:<disk>`               memory backed diff disk
190        <disk>: lower disk, e.g.: `file:base.img`
191    `file:<path>[;direct][;create=<len>]`   file-backed disk
192        <path>: path to file
193        `;direct`: bypass the OS page cache
194    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
195    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
196    `autocache:<key>:<disk>`       auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
197    `blob:<type>:<url>`            HTTP blob (read-only)
198        <type>: `flat` or `vhd1`
199    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
200        <cipher>: `xts-aes-256`
201    `prwrap:<disk>`                persistent reservations wrapper
202
203flags:
204    `ro`                           open disk as read-only
205    `vtl2`                         assign this disk to VTL2
206    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
207    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
208
209options:
210    `pcie_port=<name>`             present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
211"#)]
212    #[clap(long)]
213    pub nvme: Vec<DiskCli>,
214
215    /// attach a disk via a virtio-blk controller
216    #[clap(long_help = r#"
217e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
218
219syntax: <path> | kind:<arg>[,flag,opt=arg,...]
220
221valid disk kinds:
222    `mem:<len>`                    memory backed disk
223        <len>: length of ramdisk, e.g.: `1G`
224    `memdiff:<disk>`               memory backed diff disk
225        <disk>: lower disk, e.g.: `file:base.img`
226    `file:<path>[;direct]`                  file-backed disk
227        <path>: path to file
228        `;direct`: bypass the OS page cache
229
230flags:
231    `ro`                           open disk as read-only
232
233options:
234    `pcie_port=<name>`             present the disk using pcie under the specified port
235"#)]
236    #[clap(long = "virtio-blk")]
237    pub virtio_blk: Vec<DiskCli>,
238
239    /// Attach a vhost-user device via a Unix socket.
240    ///
241    /// The first positional argument is the socket path. Options:
242    ///
243    /// ```text
244    ///   type=blk|fs                        — device type (shorthand)
245    ///   device_id=N                        — numeric virtio device ID
246    ///   tag=NAME                           — mount tag (required for type=fs)
247    ///   num_queues=N                       — queue count (type=blk/fs only)
248    ///   queue_size=N                       — per-queue size (type=blk/fs only)
249    ///   queue_sizes=[N,N,N]                — per-queue sizes (device_id= only)
250    ///   pcie_port=NAME                     — present on PCIe under the specified port
251    /// ```
252    ///
253    /// Examples:
254    ///
255    /// ```text
256    ///   --vhost-user /tmp/vhost.sock,type=blk
257    ///   --vhost-user /tmp/vhost.sock,type=blk,num_queues=4,queue_size=512
258    ///   --vhost-user /tmp/vhost.sock,device_id=2,queue_sizes=[128,128]
259    ///   --vhost-user /tmp/vhost.sock,type=blk,pcie_port=port0
260    ///   --vhost-user /tmp/virtiofsd.sock,type=fs,tag=myfs
261    ///   --vhost-user /tmp/virtiofsd.sock,type=fs,tag=myfs,num_queues=2,queue_size=1024
262    /// ```
263    #[cfg(target_os = "linux")]
264    #[clap(long = "vhost-user")]
265    pub vhost_user: Vec<VhostUserCli>,
266
267    /// number of sub-channels for the SCSI controller
268    #[clap(long, value_name = "COUNT", default_value = "0")]
269    pub scsi_sub_channels: u16,
270
271    /// expose a virtual NIC
272    #[clap(long)]
273    pub nic: bool,
274
275    /// expose a virtual NIC with the given backend (consomme | dio | tap | none)
276    ///
277    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
278    /// `vtl2:` to assign this NIC to VTL2, or `pcie_port=<port_name>:` to
279    /// expose the NIC over emulated PCIe at the specified port.
280    #[clap(long)]
281    pub net: Vec<NicConfigCli>,
282
283    /// expose a virtual NIC using the Windows kernel-mode vmswitch.
284    ///
285    /// Specify the switch ID or "default" for the default switch.
286    #[clap(long, value_name = "SWITCH_ID")]
287    pub kernel_vmnic: Vec<String>,
288
289    /// expose a graphics device
290    #[clap(long)]
291    pub gfx: bool,
292
293    /// support a graphics device in vtl2
294    #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
295    pub vtl2_gfx: bool,
296
297    /// listen for vnc connections. implied by gfx.
298    #[clap(long)]
299    pub vnc: bool,
300
301    /// VNC port number
302    #[clap(long, value_name = "PORT", default_value = "5900")]
303    pub vnc_port: u16,
304
305    /// set the APIC ID offset, for testing APIC IDs that don't match VP index
306    #[cfg(guest_arch = "x86_64")]
307    #[clap(long, default_value_t)]
308    pub apic_id_offset: u32,
309
310    /// the maximum number of VPs per socket
311    #[clap(long)]
312    pub vps_per_socket: Option<u32>,
313
314    /// enable or disable SMT (hyperthreading) (auto | force | off)
315    #[clap(long, default_value = "auto")]
316    pub smt: SmtConfigCli,
317
318    /// configure x2apic (auto | supported | off | on)
319    #[cfg(guest_arch = "x86_64")]
320    #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
321    pub x2apic: X2ApicConfig,
322
323    /// COM1 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
324    #[clap(long, value_name = "SERIAL")]
325    pub com1: Option<SerialConfigCli>,
326
327    /// COM2 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
328    #[clap(long, value_name = "SERIAL")]
329    pub com2: Option<SerialConfigCli>,
330
331    /// COM3 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
332    #[clap(long, value_name = "SERIAL")]
333    pub com3: Option<SerialConfigCli>,
334
335    /// COM4 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
336    #[clap(long, value_name = "SERIAL")]
337    pub com4: Option<SerialConfigCli>,
338
339    /// vmbus com1 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
340    #[structopt(long, value_name = "SERIAL")]
341    pub vmbus_com1_serial: Option<SerialConfigCli>,
342
343    /// vmbus com2 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
344    #[structopt(long, value_name = "SERIAL")]
345    pub vmbus_com2_serial: Option<SerialConfigCli>,
346
347    /// Only allow guest to host serial traffic
348    #[clap(long)]
349    pub serial_tx_only: bool,
350
351    /// 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))
352    #[clap(long, value_name = "SERIAL")]
353    pub debugcon: Option<DebugconSerialConfigCli>,
354
355    /// boot UEFI firmware
356    #[clap(long, short = 'e')]
357    pub uefi: bool,
358
359    /// UEFI firmware file
360    #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
361    pub uefi_firmware: OptionalPathBuf,
362
363    /// enable UEFI debugging on COM1
364    #[clap(long, requires("uefi"))]
365    pub uefi_debug: bool,
366
367    /// enable memory protections in UEFI
368    #[clap(long, requires("uefi"))]
369    pub uefi_enable_memory_protections: bool,
370
371    /// set PCAT boot order as comma-separated string of boot device types
372    /// (e.g: floppy,hdd,optical,net).
373    ///
374    /// If less than 4 entries are added, entries are added according to their
375    /// default boot order (optical,hdd,net,floppy)
376    ///
377    /// e.g: passing "floppy,optical" will result in a boot order equivalent to
378    /// "floppy,optical,hdd,net".
379    ///
380    /// Passing duplicate types is an error.
381    #[clap(long, requires("pcat"))]
382    pub pcat_boot_order: Option<PcatBootOrderCli>,
383
384    /// Boot with PCAT BIOS firmware and piix4 devices
385    #[clap(long, conflicts_with("uefi"))]
386    pub pcat: bool,
387
388    /// PCAT firmware file
389    #[clap(long, requires("pcat"), value_name = "FILE")]
390    pub pcat_firmware: Option<PathBuf>,
391
392    /// boot IGVM file
393    #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
394    pub igvm: Option<PathBuf>,
395
396    /// specify igvm vtl2 relocation type
397    /// (absolute=\<addr\>, disable, auto=\<filesize,or memory size\>, vtl2=\<filesize,or memory size\>,)
398    #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
399    pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
400
401    /// add a virtio_9p device (e.g. myfs,C:\)
402    ///
403    /// Prefix with `pcie_port=<port_name>:` to expose the device over
404    /// emulated PCIe at the specified port.
405    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
406    pub virtio_9p: Vec<FsArgs>,
407
408    /// output debug info from the 9p server
409    #[clap(long)]
410    pub virtio_9p_debug: bool,
411
412    /// add a virtio_fs device (e.g. myfs,C:\,uid=1000,gid=2000)
413    ///
414    /// Prefix with `pcie_port=<port_name>:` to expose the device over
415    /// emulated PCIe at the specified port.
416    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
417    pub virtio_fs: Vec<FsArgsWithOptions>,
418
419    /// add a virtio_fs device for sharing memory (e.g. myfs,\SectionDirectoryPath)
420    ///
421    /// Prefix with `pcie_port=<port_name>:` to expose the device over
422    /// emulated PCIe at the specified port.
423    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
424    pub virtio_fs_shmem: Vec<FsArgs>,
425
426    /// add a virtio_fs device under either the PCI or MMIO bus, or whatever the hypervisor supports (pci | mmio | auto)
427    #[clap(long, value_name = "BUS", default_value = "auto")]
428    pub virtio_fs_bus: VirtioBusCli,
429
430    /// virtio PMEM device
431    ///
432    /// Prefix with `pcie_port=<port_name>:` to expose the device over
433    /// emulated PCIe at the specified port.
434    #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
435    pub virtio_pmem: Option<VirtioPmemArgs>,
436
437    /// add a virtio entropy (RNG) device
438    #[clap(long)]
439    pub virtio_rng: bool,
440
441    /// add a virtio-rng device under either the PCI or MMIO bus, or whatever the hypervisor supports (pci | mmio | vpci | auto)
442    #[clap(long, value_name = "BUS", default_value = "auto")]
443    pub virtio_rng_bus: VirtioBusCli,
444
445    /// attach the virtio-rng device to the specified PCIe port (overrides --virtio-rng-bus)
446    #[clap(long, value_name = "PORT", requires("virtio_rng"))]
447    pub virtio_rng_pcie_port: Option<String>,
448
449    /// virtio console device backed by a serial backend (/dev/hvc0 in guest)
450    ///
451    /// Accepts serial config (console | stderr | listen=\<path\> |
452    /// file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> |
453    /// term[=\<program\>]\[,name=\<windowtitle\>\] | none)
454    #[clap(long)]
455    pub virtio_console: Option<SerialConfigCli>,
456
457    /// attach the virtio-console device to the specified PCIe port
458    #[clap(long, value_name = "PORT", requires("virtio_console"))]
459    pub virtio_console_pcie_port: Option<String>,
460
461    /// add a virtio vsock device with the given Unix socket base path
462    #[clap(long, value_name = "PATH")]
463    pub virtio_vsock_path: Option<String>,
464
465    /// expose a virtio network with the given backend (dio | vmnic | tap |
466    /// none)
467    ///
468    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
469    /// `vtl2:` to assign this NIC to VTL2, or `pcie_port=<port_name>:` to
470    /// expose the NIC over emulated PCIe at the specified port.
471    #[clap(long)]
472    pub virtio_net: Vec<NicConfigCli>,
473
474    /// send log output from the worker process to a file instead of stderr. the file will be overwritten.
475    #[clap(long, value_name = "PATH")]
476    pub log_file: Option<PathBuf>,
477
478    /// write the process ID to the specified file on startup, and remove it on
479    /// exit. the file is not removed if the process is killed with SIGKILL or
480    /// crashes. no file locking is performed.
481    #[clap(long, value_name = "PATH")]
482    pub pidfile: Option<PathBuf>,
483
484    /// run as a ttrpc server on the specified Unix socket
485    #[clap(long, value_name = "SOCKETPATH")]
486    pub ttrpc: Option<PathBuf>,
487
488    /// run as a grpc server on the specified Unix socket
489    #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
490    pub grpc: Option<PathBuf>,
491
492    /// do not launch child processes
493    #[clap(long)]
494    pub single_process: bool,
495
496    /// device to assign (can be passed multiple times)
497    #[cfg(windows)]
498    #[clap(long, value_name = "PATH")]
499    pub device: Vec<String>,
500
501    /// instead of showing the frontpage the VM will shutdown instead
502    #[clap(long, requires("uefi"))]
503    pub disable_frontpage: bool,
504
505    /// add a vtpm device
506    #[clap(long)]
507    pub tpm: bool,
508
509    /// the mesh worker host name.
510    ///
511    /// Used internally for debugging and diagnostics.
512    #[clap(long, default_value = "control", hide(true))]
513    #[expect(clippy::option_option)]
514    pub internal_worker: Option<Option<String>>,
515
516    /// redirect the VTL 0 vmbus control plane to a proxy in VTL 2.
517    #[clap(long, requires("vtl2"))]
518    pub vmbus_redirect: bool,
519
520    /// limit the maximum protocol version allowed by vmbus; used for testing purposes
521    #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
522    pub vmbus_max_version: Option<u32>,
523
524    /// The disk to use for the VMGS.
525    ///
526    /// If this is not provided, guest state will be stored in memory.
527    #[clap(long_help = r#"
528e.g: --vmgs memdiff:file:/path/to/file.vmgs
529
530syntax: <path> | kind:<arg>[,flag]
531
532valid disk kinds:
533    `mem:<len>`                     memory backed disk
534        <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
535    `memdiff:<disk>[;create=<len>]` memory backed diff disk
536        <disk>: lower disk, e.g.: `file:base.img`
537    `file:<path>`                   file-backed disk
538        <path>: path to file
539
540flags:
541    `fmt`                           reprovision the VMGS before boot
542    `fmt-on-fail`                   reprovision the VMGS before boot if it is corrupted
543"#)]
544    #[clap(long)]
545    pub vmgs: Option<VmgsCli>,
546
547    /// Use GspById guest state encryption policy with a test seed
548    #[clap(long, requires("vmgs"))]
549    pub test_gsp_by_id: bool,
550
551    /// VGA firmware file
552    #[clap(long, requires("pcat"), value_name = "FILE")]
553    pub vga_firmware: Option<PathBuf>,
554
555    /// enable secure boot
556    #[clap(long)]
557    pub secure_boot: bool,
558
559    /// use secure boot template
560    #[clap(long)]
561    pub secure_boot_template: Option<SecureBootTemplateCli>,
562
563    /// custom uefi nvram json file
564    #[clap(long, value_name = "PATH")]
565    pub custom_uefi_json: Option<PathBuf>,
566
567    /// the path to a named pipe (Windows) or Unix socket (Linux) to relay to the connected
568    /// tty.
569    ///
570    /// This is a hidden argument used internally.
571    #[clap(long, hide(true))]
572    pub relay_console_path: Option<PathBuf>,
573
574    /// the title of the console window spawned from the relay console.
575    ///
576    /// This is a hidden argument used internally.
577    #[clap(long, hide(true))]
578    pub relay_console_title: Option<String>,
579
580    /// enable in-hypervisor gdb debugger
581    #[clap(long, value_name = "PORT")]
582    pub gdb: Option<u16>,
583
584    /// enable emulated MANA devices with the given network backend (see --net)
585    ///
586    /// Prefix with `pcie_port=<port_name>:` to expose the nic over emulated PCIe
587    /// at the specified port.
588    #[clap(long)]
589    pub mana: Vec<NicConfigCli>,
590
591    /// use a specific hypervisor interface, with optional backend-specific
592    /// parameters.
593    ///
594    /// Format: `name` or `name:key=val,key,...`
595    ///
596    /// WHP parameters (x86_64 guests only):
597    ///   user_mode_apic       - use user-mode APIC emulator
598    ///   no_enlightenments    - disable in-hypervisor enlightenments
599    ///
600    /// Examples:
601    ///   --hypervisor whp
602    ///   --hypervisor whp:user_mode_apic
603    ///   --hypervisor whp:user_mode_apic,no_enlightenments
604    #[clap(long)]
605    pub hypervisor: Option<String>,
606
607    /// (dev utility) boot linux using a custom (raw) DSDT table.
608    ///
609    /// This is a _very_ niche utility, and it's unlikely you'll need to use it.
610    ///
611    /// e.g: this flag helped bring up certain Hyper-V Generation 1 legacy
612    /// devices without needing to port the associated ACPI code into OpenVMM's
613    /// DSDT builder.
614    #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
615    pub custom_dsdt: Option<PathBuf>,
616
617    /// attach an ide drive (can be passed multiple times)
618    ///
619    /// Each ide controller has two channels. Each channel can have up to two
620    /// attachments.
621    ///
622    /// If the `s` flag is not passed then the drive will we be attached to the
623    /// primary ide channel if space is available. If two attachments have already
624    /// been added to the primary channel then the drive will be attached to the
625    /// secondary channel.
626    #[clap(long_help = r#"
627e.g: --ide memdiff:file:/path/to/disk.vhd
628
629syntax: <path> | kind:<arg>[,flag,opt=arg,...]
630
631valid disk kinds:
632    `mem:<len>`                    memory backed disk
633        <len>: length of ramdisk, e.g.: `1G`
634    `memdiff:<disk>`               memory backed diff disk
635        <disk>: lower disk, e.g.: `file:base.img`
636    `file:<path>[;create=<len>]`   file-backed disk
637        <path>: path to file
638    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
639    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
640    `blob:<type>:<url>`            HTTP blob (read-only)
641        <type>: `flat` or `vhd1`
642    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
643        <cipher>: `xts-aes-256`
644
645additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
646this list is not exhaustive.
647
648flags:
649    `ro`                           open disk as read-only
650    `s`                            attach drive to secondary ide channel
651    `dvd`                          specifies that device is cd/dvd and it is read_only
652"#)]
653    #[clap(long, value_name = "FILE", requires("pcat"))]
654    pub ide: Vec<IdeDiskCli>,
655
656    /// attach a floppy drive (should be able to be passed multiple times). VM must be generation 1 (no UEFI)
657    ///
658    #[clap(long_help = r#"
659e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
660
661syntax: <path> | kind:<arg>[,flag,opt=arg,...]
662
663valid disk kinds:
664    `mem:<len>`                    memory backed disk
665        <len>: length of ramdisk, e.g.: `1G`
666    `memdiff:<disk>`               memory backed diff disk
667        <disk>: lower disk, e.g.: `file:base.img`
668    `file:<path>[;create=<len>]`   file-backed disk
669        <path>: path to file
670    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
671    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
672    `blob:<type>:<url>`            HTTP blob (read-only)
673        <type>: `flat` or `vhd1`
674    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
675        <cipher>: `xts-aes-256`
676
677flags:
678    `ro`                           open disk as read-only
679"#)]
680    #[clap(long, value_name = "FILE", requires("pcat"))]
681    pub floppy: Vec<FloppyDiskCli>,
682
683    /// enable guest watchdog device
684    #[clap(long)]
685    pub guest_watchdog: bool,
686
687    /// enable OpenHCL's guest crash dump device, targeting the specified path
688    #[clap(long)]
689    pub openhcl_dump_path: Option<PathBuf>,
690
691    /// halt the VM when the guest requests a reset, instead of resetting it
692    #[clap(long)]
693    pub halt_on_reset: bool,
694
695    /// write saved state .proto files to the specified path
696    #[clap(long)]
697    pub write_saved_state_proto: Option<PathBuf>,
698
699    /// specify the IMC hive file for booting Windows
700    #[clap(long)]
701    pub imc: Option<PathBuf>,
702
703    /// expose a battery device
704    #[clap(long)]
705    pub battery: bool,
706
707    /// set the uefi console mode
708    #[clap(long)]
709    pub uefi_console_mode: Option<UefiConsoleModeCli>,
710
711    /// set the EFI diagnostics log level
712    #[clap(long_help = r#"
713Set the EFI diagnostics log level.
714
715options:
716    default                        default (ERROR and WARN only)
717    info                           info (ERROR, WARN, and INFO)
718    full                           full (all log levels)
719"#)]
720    #[clap(long, requires("uefi"))]
721    pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
722
723    /// Perform a default boot even if boot entries exist and fail
724    #[clap(long)]
725    pub default_boot_always_attempt: bool,
726
727    /// Attach a PCI Express root complex to the VM
728    #[clap(long_help = r#"
729Attach root complexes to the VM.
730
731Examples:
732    # Attach root complex rc0 on segment 0 with bus and MMIO ranges
733    --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
734
735Syntax: <name>[,opt=arg,...]
736
737Options:
738    `segment=<value>`              configures the PCI Express segment, default 0
739    `start_bus=<value>`            lowest valid bus number, default 0
740    `end_bus=<value>`              highest valid bus number, default 255
741    `low_mmio=<size>`              low MMIO window size, default 64M
742    `high_mmio=<size>`             high MMIO window size, default 1G
743"#)]
744    #[clap(long, conflicts_with("pcat"))]
745    pub pcie_root_complex: Vec<PcieRootComplexCli>,
746
747    /// Attach a PCI Express root port to the VM
748    #[clap(long_help = r#"
749Attach root ports to root complexes.
750
751Examples:
752    # Attach root port rc0rp0 to root complex rc0
753    --pcie-root-port rc0:rc0rp0
754
755    # Attach root port rc0rp1 to root complex rc0 with hotplug support
756    --pcie-root-port rc0:rc0rp1,hotplug
757
758Syntax: <root_complex_name>:<name>[,hotplug]
759
760Options:
761    `hotplug`                      enable hotplug support for this root port
762"#)]
763    #[clap(long, conflicts_with("pcat"))]
764    pub pcie_root_port: Vec<PcieRootPortCli>,
765
766    /// Attach a PCI Express switch to the VM
767    #[clap(long_help = r#"
768Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
769
770Examples:
771    # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
772    --pcie-switch rp0:switch0,num_downstream_ports=4
773
774    # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
775    --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
776
777    # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
778    --pcie-switch rp0:switch0
779    --pcie-switch switch0-downstream-0:switch1
780    --pcie-switch switch1-downstream-1:switch2
781
782    # Enable hotplug on all downstream switch ports of switch0
783    --pcie-switch rp0:switch0,hotplug
784
785Syntax: <port_name>:<name>[,opt,opt=arg,...]
786
787    port_name can be:
788        - Root port name (e.g., "rp0") to connect directly to a root port
789        - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
790
791Options:
792    `hotplug`                       enable hotplug support for all downstream switch ports
793    `num_downstream_ports=<value>`  number of downstream ports, default 4
794"#)]
795    #[clap(long, conflicts_with("pcat"))]
796    pub pcie_switch: Vec<GenericPcieSwitchCli>,
797
798    /// Attach a PCIe remote device to a downstream port
799    #[clap(long_help = r#"
800Attach PCIe devices to root ports or downstream switch ports
801which are implemented in a simulator running in a remote process.
802
803Examples:
804    # Attach to root port rc0rp0 with default socket
805    --pcie-remote rc0rp0
806
807    # Attach with custom socket address
808    --pcie-remote rc0rp0,socket=0.0.0.0:48914
809
810    # Specify HU and controller identifiers
811    --pcie-remote rc0rp0,hu=1,controller=0
812
813    # Multiple devices on different ports
814    --pcie-remote rc0rp0,socket=0.0.0.0:48914
815    --pcie-remote rc0rp1,socket=0.0.0.0:48915
816
817Syntax: <port_name>[,opt=arg,...]
818
819Options:
820    `socket=<address>`              TCP socket (default: localhost:48914)
821    `hu=<value>`                    Hardware unit identifier (default: 0)
822    `controller=<value>`            Controller identifier (default: 0)
823"#)]
824    #[clap(long, conflicts_with("pcat"))]
825    pub pcie_remote: Vec<PcieRemoteCli>,
826
827    /// Assign a host PCI device to the guest via VFIO (Linux only)
828    #[clap(long_help = r#"
829Assign a host PCI device to the guest via Linux VFIO.
830
831The device must be bound to vfio-pci on the host before starting the VM.
832
833Examples:
834    # Assign NVMe controller to root port rp0
835    --vfio rp0:0000:01:00.0
836
837Syntax: <port_name>:<pci_bdf>
838
839    port_name    Root port or downstream switch port name
840    pci_bdf      PCI domain:bus:device.function of the VFIO device on
841                 the host (use lspci -D to find it)
842"#)]
843    #[cfg(target_os = "linux")]
844    #[clap(long, conflicts_with("pcat"))]
845    pub vfio: Vec<VfioDeviceCli>,
846}
847
848#[derive(Clone, Debug, PartialEq)]
849pub struct FsArgs {
850    pub tag: String,
851    pub path: String,
852    pub pcie_port: Option<String>,
853}
854
855impl FromStr for FsArgs {
856    type Err = anyhow::Error;
857
858    fn from_str(s: &str) -> Result<Self, Self::Err> {
859        let (pcie_port, s) = parse_pcie_port_prefix(s);
860        let mut s = s.split(',');
861        let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
862            anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
863        };
864        Ok(Self {
865            tag: tag.to_owned(),
866            path: path.to_owned(),
867            pcie_port,
868        })
869    }
870}
871
872#[derive(Clone, Debug, PartialEq)]
873pub struct FsArgsWithOptions {
874    /// The file system tag.
875    pub tag: String,
876    /// The root path.
877    pub path: String,
878    /// The extra options, joined with ';'.
879    pub options: String,
880    /// Optional PCIe port name.
881    pub pcie_port: Option<String>,
882}
883
884impl FromStr for FsArgsWithOptions {
885    type Err = anyhow::Error;
886
887    fn from_str(s: &str) -> Result<Self, Self::Err> {
888        let (pcie_port, s) = parse_pcie_port_prefix(s);
889        let mut s = s.split(',');
890        let (Some(tag), Some(path)) = (s.next(), s.next()) else {
891            anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
892        };
893        let options = s.collect::<Vec<_>>().join(";");
894        Ok(Self {
895            tag: tag.to_owned(),
896            path: path.to_owned(),
897            options,
898            pcie_port,
899        })
900    }
901}
902
903#[derive(Copy, Clone, clap::ValueEnum)]
904pub enum VirtioBusCli {
905    Auto,
906    Mmio,
907    Pci,
908    Vpci,
909}
910
911/// Parse an optional `pcie_port=<name>:` prefix from a CLI argument string.
912///
913/// Returns `(Some(port_name), rest)` if the prefix is present, or
914/// `(None, original)` if not.
915fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
916    if let Some(rest) = s.strip_prefix("pcie_port=") {
917        if let Some((port, rest)) = rest.split_once(':') {
918            if !port.is_empty() {
919                return (Some(port.to_string()), rest);
920            }
921        }
922    }
923    (None, s)
924}
925
926#[derive(Clone, Debug, PartialEq)]
927pub struct VirtioPmemArgs {
928    pub path: String,
929    pub pcie_port: Option<String>,
930}
931
932impl FromStr for VirtioPmemArgs {
933    type Err = anyhow::Error;
934
935    fn from_str(s: &str) -> Result<Self, Self::Err> {
936        let (pcie_port, s) = parse_pcie_port_prefix(s);
937        if s.is_empty() {
938            anyhow::bail!("expected [pcie_port=<port>:]<path>");
939        }
940        Ok(Self {
941            path: s.to_owned(),
942            pcie_port,
943        })
944    }
945}
946
947#[derive(clap::ValueEnum, Clone, Copy)]
948pub enum SecureBootTemplateCli {
949    Windows,
950    UefiCa,
951}
952
953fn parse_memory(s: &str) -> anyhow::Result<u64> {
954    if s == "VMGS_DEFAULT" {
955        Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
956    } else {
957        || -> Option<u64> {
958            let mut b = s.as_bytes();
959            if s.ends_with('B') {
960                b = &b[..b.len() - 1]
961            }
962            if b.is_empty() {
963                return None;
964            }
965            let multi = match b[b.len() - 1] as char {
966                'T' => Some(1024 * 1024 * 1024 * 1024),
967                'G' => Some(1024 * 1024 * 1024),
968                'M' => Some(1024 * 1024),
969                'K' => Some(1024),
970                _ => None,
971            };
972            if multi.is_some() {
973                b = &b[..b.len() - 1]
974            }
975            let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
976            Some(n * multi.unwrap_or(1))
977        }()
978        .with_context(|| format!("invalid memory size '{0}'", s))
979    }
980}
981
982/// Parse a number from a string that could be prefixed with 0x to indicate hex.
983fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
984    match s.strip_prefix("0x") {
985        Some(rest) => u64::from_str_radix(rest, 16),
986        None => s.parse::<u64>(),
987    }
988}
989
990#[derive(Clone, Debug, PartialEq)]
991pub enum DiskCliKind {
992    // mem:<len>
993    Memory(u64),
994    // memdiff:<kind>
995    MemoryDiff(Box<DiskCliKind>),
996    // sql:<path>[;create=<len>]
997    Sqlite {
998        path: PathBuf,
999        create_with_len: Option<u64>,
1000    },
1001    // sqldiff:<path>[;create]:<kind>
1002    SqliteDiff {
1003        path: PathBuf,
1004        create: bool,
1005        disk: Box<DiskCliKind>,
1006    },
1007    // autocache:[key]:<kind>
1008    AutoCacheSqlite {
1009        cache_path: String,
1010        key: Option<String>,
1011        disk: Box<DiskCliKind>,
1012    },
1013    // prwrap:<kind>
1014    PersistentReservationsWrapper(Box<DiskCliKind>),
1015    // file:<path>[;direct][;create=<len>]
1016    File {
1017        path: PathBuf,
1018        create_with_len: Option<u64>,
1019        direct: bool,
1020    },
1021    // blob:<type>:<url>
1022    Blob {
1023        kind: BlobKind,
1024        url: String,
1025    },
1026    // crypt:<cipher>:<key_file>:<kind>
1027    Crypt {
1028        cipher: DiskCipher,
1029        key_file: PathBuf,
1030        disk: Box<DiskCliKind>,
1031    },
1032    // delay:<delay_ms>:<kind>
1033    DelayDiskWrapper {
1034        delay_ms: u64,
1035        disk: Box<DiskCliKind>,
1036    },
1037}
1038
1039#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
1040pub enum DiskCipher {
1041    #[clap(name = "xts-aes-256")]
1042    XtsAes256,
1043}
1044
1045#[derive(Copy, Clone, Debug, PartialEq)]
1046pub enum BlobKind {
1047    Flat,
1048    Vhd1,
1049}
1050
1051struct FileOpts {
1052    path: PathBuf,
1053    create_with_len: Option<u64>,
1054    direct: bool,
1055}
1056
1057fn parse_file_opts(arg: &str) -> anyhow::Result<FileOpts> {
1058    let mut path = arg;
1059    let mut create_with_len = None;
1060    let mut direct = false;
1061
1062    // Parse semicolon-delimited options after the path.
1063    if let Some((p, rest)) = arg.split_once(';') {
1064        path = p;
1065        for opt in rest.split(';') {
1066            if let Some(len) = opt.strip_prefix("create=") {
1067                create_with_len = Some(parse_memory(len)?);
1068            } else if opt == "direct" {
1069                direct = true;
1070            } else {
1071                anyhow::bail!("invalid file option '{opt}', expected 'create=<len>' or 'direct'");
1072            }
1073        }
1074    }
1075
1076    Ok(FileOpts {
1077        path: path.into(),
1078        create_with_len,
1079        direct,
1080    })
1081}
1082
1083impl DiskCliKind {
1084    /// Parse an `autocache:[key]:<kind>` disk spec, given the cache path
1085    /// (normally read from `OPENVMM_AUTO_CACHE_PATH`).
1086    fn parse_autocache(
1087        arg: &str,
1088        cache_path: Result<String, std::env::VarError>,
1089    ) -> anyhow::Result<Self> {
1090        let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
1091        let cache_path = cache_path.context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
1092        Ok(DiskCliKind::AutoCacheSqlite {
1093            cache_path,
1094            key: (!key.is_empty()).then(|| key.to_string()),
1095            disk: Box::new(kind.parse()?),
1096        })
1097    }
1098}
1099
1100impl FromStr for DiskCliKind {
1101    type Err = anyhow::Error;
1102
1103    fn from_str(s: &str) -> anyhow::Result<Self> {
1104        let disk = match s.split_once(':') {
1105            // convenience support for passing bare paths as file disks
1106            None => {
1107                let FileOpts {
1108                    path,
1109                    create_with_len,
1110                    direct,
1111                } = parse_file_opts(s)?;
1112                DiskCliKind::File {
1113                    path,
1114                    create_with_len,
1115                    direct,
1116                }
1117            }
1118            Some((kind, arg)) => match kind {
1119                "mem" => DiskCliKind::Memory(parse_memory(arg)?),
1120                "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
1121                "sql" => {
1122                    let FileOpts {
1123                        path,
1124                        create_with_len,
1125                        direct,
1126                    } = parse_file_opts(arg)?;
1127                    if direct {
1128                        anyhow::bail!("'direct' is not supported for 'sql' disks");
1129                    }
1130                    DiskCliKind::Sqlite {
1131                        path,
1132                        create_with_len,
1133                    }
1134                }
1135                "sqldiff" => {
1136                    let (path_and_opts, kind) =
1137                        arg.split_once(':').context("expected path[;opts]:kind")?;
1138                    let disk = Box::new(kind.parse()?);
1139                    match path_and_opts.split_once(';') {
1140                        Some((path, create)) => {
1141                            if create != "create" {
1142                                anyhow::bail!("invalid syntax after ';', expected 'create'")
1143                            }
1144                            DiskCliKind::SqliteDiff {
1145                                path: path.into(),
1146                                create: true,
1147                                disk,
1148                            }
1149                        }
1150                        None => DiskCliKind::SqliteDiff {
1151                            path: path_and_opts.into(),
1152                            create: false,
1153                            disk,
1154                        },
1155                    }
1156                }
1157                "autocache" => {
1158                    Self::parse_autocache(arg, std::env::var("OPENVMM_AUTO_CACHE_PATH"))?
1159                }
1160                "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
1161                "file" => {
1162                    let FileOpts {
1163                        path,
1164                        create_with_len,
1165                        direct,
1166                    } = parse_file_opts(arg)?;
1167                    DiskCliKind::File {
1168                        path,
1169                        create_with_len,
1170                        direct,
1171                    }
1172                }
1173                "blob" => {
1174                    let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
1175                    let blob_kind = match blob_kind {
1176                        "flat" => BlobKind::Flat,
1177                        "vhd1" => BlobKind::Vhd1,
1178                        _ => anyhow::bail!("unknown blob kind {blob_kind}"),
1179                    };
1180                    DiskCliKind::Blob {
1181                        kind: blob_kind,
1182                        url: url.to_string(),
1183                    }
1184                }
1185                "crypt" => {
1186                    let (cipher, (key, kind)) = arg
1187                        .split_once(':')
1188                        .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
1189                        .context("expected cipher:key_file:kind")?;
1190                    DiskCliKind::Crypt {
1191                        cipher: ValueEnum::from_str(cipher, false)
1192                            .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
1193                        key_file: PathBuf::from(key),
1194                        disk: Box::new(kind.parse()?),
1195                    }
1196                }
1197                kind => {
1198                    // here's a fun edge case: what if the user passes `--disk d:\path\to\disk.img`?
1199                    //
1200                    // in this case, we actually want to treat that leading `d:` as part of the
1201                    // path, rather than as a disk with `kind == 'd'`
1202                    let FileOpts {
1203                        path,
1204                        create_with_len,
1205                        direct,
1206                    } = parse_file_opts(s)?;
1207                    if path.has_root() {
1208                        DiskCliKind::File {
1209                            path,
1210                            create_with_len,
1211                            direct,
1212                        }
1213                    } else {
1214                        anyhow::bail!("invalid disk kind {kind}");
1215                    }
1216                }
1217            },
1218        };
1219        Ok(disk)
1220    }
1221}
1222
1223#[derive(Clone)]
1224pub struct VmgsCli {
1225    pub kind: DiskCliKind,
1226    pub provision: ProvisionVmgs,
1227}
1228
1229#[derive(Copy, Clone)]
1230pub enum ProvisionVmgs {
1231    OnEmpty,
1232    OnFailure,
1233    True,
1234}
1235
1236impl FromStr for VmgsCli {
1237    type Err = anyhow::Error;
1238
1239    fn from_str(s: &str) -> anyhow::Result<Self> {
1240        let (kind, opt) = s
1241            .split_once(',')
1242            .map(|(k, o)| (k, Some(o)))
1243            .unwrap_or((s, None));
1244        let kind = kind.parse()?;
1245
1246        let provision = match opt {
1247            None => ProvisionVmgs::OnEmpty,
1248            Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
1249            Some("fmt") => ProvisionVmgs::True,
1250            Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
1251        };
1252
1253        Ok(VmgsCli { kind, provision })
1254    }
1255}
1256
1257// <kind>[,ro]
1258#[derive(Clone)]
1259pub struct DiskCli {
1260    pub vtl: DeviceVtl,
1261    pub kind: DiskCliKind,
1262    pub read_only: bool,
1263    pub is_dvd: bool,
1264    pub underhill: Option<UnderhillDiskSource>,
1265    pub pcie_port: Option<String>,
1266}
1267
1268#[derive(Copy, Clone)]
1269pub enum UnderhillDiskSource {
1270    Scsi,
1271    Nvme,
1272}
1273
1274impl FromStr for DiskCli {
1275    type Err = anyhow::Error;
1276
1277    fn from_str(s: &str) -> anyhow::Result<Self> {
1278        let mut opts = s.split(',');
1279        let kind = opts.next().unwrap().parse()?;
1280
1281        let mut read_only = false;
1282        let mut is_dvd = false;
1283        let mut underhill = None;
1284        let mut vtl = DeviceVtl::Vtl0;
1285        let mut pcie_port = None;
1286        for opt in opts {
1287            let mut s = opt.split('=');
1288            let opt = s.next().unwrap();
1289            match opt {
1290                "ro" => read_only = true,
1291                "dvd" => {
1292                    is_dvd = true;
1293                    read_only = true;
1294                }
1295                "vtl2" => {
1296                    vtl = DeviceVtl::Vtl2;
1297                }
1298                "uh" => underhill = Some(UnderhillDiskSource::Scsi),
1299                "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
1300                "pcie_port" => {
1301                    let port = s.next();
1302                    if port.is_none_or(|p| p.is_empty()) {
1303                        anyhow::bail!("`pcie_port` requires a port name");
1304                    }
1305                    pcie_port = Some(String::from(port.unwrap()));
1306                }
1307                opt => anyhow::bail!("unknown option: '{opt}'"),
1308            }
1309        }
1310
1311        if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
1312            anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
1313        }
1314
1315        if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
1316            anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
1317        }
1318
1319        Ok(DiskCli {
1320            vtl,
1321            kind,
1322            read_only,
1323            is_dvd,
1324            underhill,
1325            pcie_port,
1326        })
1327    }
1328}
1329
1330// <kind>[,ro,s]
1331#[derive(Clone)]
1332pub struct IdeDiskCli {
1333    pub kind: DiskCliKind,
1334    pub read_only: bool,
1335    pub channel: Option<u8>,
1336    pub device: Option<u8>,
1337    pub is_dvd: bool,
1338}
1339
1340impl FromStr for IdeDiskCli {
1341    type Err = anyhow::Error;
1342
1343    fn from_str(s: &str) -> anyhow::Result<Self> {
1344        let mut opts = s.split(',');
1345        let kind = opts.next().unwrap().parse()?;
1346
1347        let mut read_only = false;
1348        let mut channel = None;
1349        let mut device = None;
1350        let mut is_dvd = false;
1351        for opt in opts {
1352            let mut s = opt.split('=');
1353            let opt = s.next().unwrap();
1354            match opt {
1355                "ro" => read_only = true,
1356                "p" => channel = Some(0),
1357                "s" => channel = Some(1),
1358                "0" => device = Some(0),
1359                "1" => device = Some(1),
1360                "dvd" => {
1361                    is_dvd = true;
1362                    read_only = true;
1363                }
1364                _ => anyhow::bail!("unknown option: '{opt}'"),
1365            }
1366        }
1367
1368        Ok(IdeDiskCli {
1369            kind,
1370            read_only,
1371            channel,
1372            device,
1373            is_dvd,
1374        })
1375    }
1376}
1377
1378// <kind>[,ro]
1379#[derive(Clone, Debug, PartialEq)]
1380pub struct FloppyDiskCli {
1381    pub kind: DiskCliKind,
1382    pub read_only: bool,
1383}
1384
1385impl FromStr for FloppyDiskCli {
1386    type Err = anyhow::Error;
1387
1388    fn from_str(s: &str) -> anyhow::Result<Self> {
1389        if s.is_empty() {
1390            anyhow::bail!("empty disk spec");
1391        }
1392        let mut opts = s.split(',');
1393        let kind = opts.next().unwrap().parse()?;
1394
1395        let mut read_only = false;
1396        for opt in opts {
1397            let mut s = opt.split('=');
1398            let opt = s.next().unwrap();
1399            match opt {
1400                "ro" => read_only = true,
1401                _ => anyhow::bail!("unknown option: '{opt}'"),
1402            }
1403        }
1404
1405        Ok(FloppyDiskCli { kind, read_only })
1406    }
1407}
1408
1409#[derive(Clone)]
1410pub struct DebugconSerialConfigCli {
1411    pub port: u16,
1412    pub serial: SerialConfigCli,
1413}
1414
1415impl FromStr for DebugconSerialConfigCli {
1416    type Err = String;
1417
1418    fn from_str(s: &str) -> Result<Self, Self::Err> {
1419        let Some((port, serial)) = s.split_once(',') else {
1420            return Err("invalid format (missing comma between port and serial)".into());
1421        };
1422
1423        let port: u16 = parse_number(port)
1424            .map_err(|_| "could not parse port".to_owned())?
1425            .try_into()
1426            .map_err(|_| "port must be 16-bit")?;
1427        let serial: SerialConfigCli = serial.parse()?;
1428
1429        Ok(Self { port, serial })
1430    }
1431}
1432
1433/// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
1434#[derive(Clone, Debug, PartialEq)]
1435pub enum SerialConfigCli {
1436    None,
1437    Console,
1438    NewConsole(Option<PathBuf>, Option<String>),
1439    Stderr,
1440    Pipe(PathBuf),
1441    Tcp(SocketAddr),
1442    File(PathBuf),
1443}
1444
1445impl FromStr for SerialConfigCli {
1446    type Err = String;
1447
1448    fn from_str(s: &str) -> Result<Self, Self::Err> {
1449        let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1450
1451        let first_key = match keyvalues.first() {
1452            Some(first_pair) => first_pair.0.as_str(),
1453            None => Err("invalid serial configuration: no values supplied")?,
1454        };
1455        let first_value = keyvalues.first().unwrap().1.as_ref();
1456
1457        let ret = match first_key {
1458            "none" => SerialConfigCli::None,
1459            "console" => SerialConfigCli::Console,
1460            "stderr" => SerialConfigCli::Stderr,
1461            "file" => match first_value {
1462                Some(path) => SerialConfigCli::File(path.into()),
1463                None => Err("invalid serial configuration: file requires a value")?,
1464            },
1465            "term" => {
1466                // If user supplies a name key, use it to title the window
1467                let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1468                let window_name = match window_name {
1469                    Some((_, Some(name))) => Some(name.clone()),
1470                    _ => None,
1471                };
1472
1473                SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
1474            }
1475            "listen" => match first_value {
1476                Some(path) => {
1477                    if let Some(tcp) = path.strip_prefix("tcp:") {
1478                        let addr = tcp
1479                            .parse()
1480                            .map_err(|err| format!("invalid tcp address: {err}"))?;
1481                        SerialConfigCli::Tcp(addr)
1482                    } else {
1483                        SerialConfigCli::Pipe(path.into())
1484                    }
1485                }
1486                None => Err(
1487                    "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1488                )?,
1489            },
1490            _ => {
1491                return Err(format!(
1492                    "invalid serial configuration: '{}' is not a known option",
1493                    first_key
1494                ));
1495            }
1496        };
1497
1498        Ok(ret)
1499    }
1500}
1501
1502impl SerialConfigCli {
1503    /// Parse a comma separated list of key=value options into a vector of
1504    /// key/value pairs.
1505    fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1506        let mut ret = Vec::new();
1507
1508        // For each comma separated item in the supplied list
1509        for item in s.split(',') {
1510            // Split on the = for key and value
1511            // If no = is found, treat key as key and value as None
1512            let mut eqsplit = item.split('=');
1513            let key = eqsplit.next();
1514            let value = eqsplit.next();
1515
1516            if let Some(key) = key {
1517                ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1518            } else {
1519                // An empty key is invalid
1520                return Err("invalid key=value pair in serial config".into());
1521            }
1522        }
1523        Ok(ret)
1524    }
1525}
1526
1527#[derive(Clone, Debug, PartialEq)]
1528pub enum EndpointConfigCli {
1529    None,
1530    Consomme { cidr: Option<String> },
1531    Dio { id: Option<String> },
1532    Tap { name: String },
1533}
1534
1535impl FromStr for EndpointConfigCli {
1536    type Err = String;
1537
1538    fn from_str(s: &str) -> Result<Self, Self::Err> {
1539        let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1540            ["none"] => EndpointConfigCli::None,
1541            ["consomme", s @ ..] => EndpointConfigCli::Consomme {
1542                cidr: s.first().map(|&s| s.to_owned()),
1543            },
1544            ["dio", s @ ..] => EndpointConfigCli::Dio {
1545                id: s.first().map(|s| (*s).to_owned()),
1546            },
1547            ["tap", name] => EndpointConfigCli::Tap {
1548                name: (*name).to_owned(),
1549            },
1550            _ => return Err("invalid network backend".into()),
1551        };
1552
1553        Ok(ret)
1554    }
1555}
1556
1557#[derive(Clone, Debug, PartialEq)]
1558pub struct NicConfigCli {
1559    pub vtl: DeviceVtl,
1560    pub endpoint: EndpointConfigCli,
1561    pub max_queues: Option<u16>,
1562    pub underhill: bool,
1563    pub pcie_port: Option<String>,
1564}
1565
1566impl FromStr for NicConfigCli {
1567    type Err = String;
1568
1569    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1570        let mut vtl = DeviceVtl::Vtl0;
1571        let mut max_queues = None;
1572        let mut underhill = false;
1573        let mut pcie_port = None;
1574        while let Some((opt, rest)) = s.split_once(':') {
1575            if let Some((opt, val)) = opt.split_once('=') {
1576                match opt {
1577                    "queues" => {
1578                        max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1579                    }
1580                    "pcie_port" => {
1581                        if val.is_empty() {
1582                            return Err("`pcie_port=` requires port name argument".into());
1583                        }
1584                        pcie_port = Some(val.to_string());
1585                    }
1586                    _ => break,
1587                }
1588            } else {
1589                match opt {
1590                    "vtl2" => {
1591                        vtl = DeviceVtl::Vtl2;
1592                    }
1593                    "uh" => underhill = true,
1594                    _ => break,
1595                }
1596            }
1597            s = rest;
1598        }
1599
1600        if underhill && vtl != DeviceVtl::Vtl0 {
1601            return Err("`uh` is incompatible with `vtl2`".into());
1602        }
1603
1604        if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
1605            return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
1606        }
1607
1608        let endpoint = s.parse()?;
1609        Ok(NicConfigCli {
1610            vtl,
1611            endpoint,
1612            max_queues,
1613            underhill,
1614            pcie_port,
1615        })
1616    }
1617}
1618
1619#[derive(Debug, Error)]
1620#[error("unknown VTL2 relocation type: {0}")]
1621pub struct UnknownVtl2RelocationType(String);
1622
1623fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1624    match s {
1625        "disable" => Ok(Vtl2BaseAddressType::File),
1626        s if s.starts_with("auto=") => {
1627            let s = s.strip_prefix("auto=").unwrap_or_default();
1628            let size = if s == "filesize" {
1629                None
1630            } else {
1631                let size = parse_memory(s).map_err(|e| {
1632                    UnknownVtl2RelocationType(format!(
1633                        "unable to parse memory size from {} for 'auto=' type, {e}",
1634                        e
1635                    ))
1636                })?;
1637                Some(size)
1638            };
1639            Ok(Vtl2BaseAddressType::MemoryLayout { size })
1640        }
1641        s if s.starts_with("absolute=") => {
1642            let s = s.strip_prefix("absolute=");
1643            let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
1644                UnknownVtl2RelocationType(format!(
1645                    "unable to parse number from {} for 'absolute=' type",
1646                    e
1647                ))
1648            })?;
1649            Ok(Vtl2BaseAddressType::Absolute(addr))
1650        }
1651        s if s.starts_with("vtl2=") => {
1652            let s = s.strip_prefix("vtl2=").unwrap_or_default();
1653            let size = if s == "filesize" {
1654                None
1655            } else {
1656                let size = parse_memory(s).map_err(|e| {
1657                    UnknownVtl2RelocationType(format!(
1658                        "unable to parse memory size from {} for 'vtl2=' type, {e}",
1659                        e
1660                    ))
1661                })?;
1662                Some(size)
1663            };
1664            Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
1665        }
1666        _ => Err(UnknownVtl2RelocationType(s.to_owned())),
1667    }
1668}
1669
1670#[derive(Debug, Copy, Clone, PartialEq)]
1671pub enum SmtConfigCli {
1672    Auto,
1673    Force,
1674    Off,
1675}
1676
1677#[derive(Debug, Error)]
1678#[error("expected auto, force, or off")]
1679pub struct BadSmtConfig;
1680
1681impl FromStr for SmtConfigCli {
1682    type Err = BadSmtConfig;
1683
1684    fn from_str(s: &str) -> Result<Self, Self::Err> {
1685        let r = match s {
1686            "auto" => Self::Auto,
1687            "force" => Self::Force,
1688            "off" => Self::Off,
1689            _ => return Err(BadSmtConfig),
1690        };
1691        Ok(r)
1692    }
1693}
1694
1695#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
1696fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
1697    let r = match s {
1698        "auto" => X2ApicConfig::Auto,
1699        "supported" => X2ApicConfig::Supported,
1700        "off" => X2ApicConfig::Unsupported,
1701        "on" => X2ApicConfig::Enabled,
1702        _ => return Err("expected auto, supported, off, or on"),
1703    };
1704    Ok(r)
1705}
1706
1707#[derive(Debug, Copy, Clone, ValueEnum)]
1708pub enum Vtl0LateMapPolicyCli {
1709    Off,
1710    Log,
1711    Halt,
1712    Exception,
1713}
1714
1715#[derive(Debug, Copy, Clone, ValueEnum)]
1716pub enum IsolationCli {
1717    Vbs,
1718}
1719
1720#[derive(Debug, Copy, Clone, PartialEq)]
1721pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
1722
1723impl FromStr for PcatBootOrderCli {
1724    type Err = &'static str;
1725
1726    fn from_str(s: &str) -> Result<Self, Self::Err> {
1727        let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
1728        let mut order = Vec::new();
1729
1730        for item in s.split(',') {
1731            let device = match item {
1732                "optical" => PcatBootDevice::Optical,
1733                "hdd" => PcatBootDevice::HardDrive,
1734                "net" => PcatBootDevice::Network,
1735                "floppy" => PcatBootDevice::Floppy,
1736                _ => return Err("unknown boot device type"),
1737            };
1738
1739            let default_pos = default_order
1740                .iter()
1741                .position(|x| x == &Some(device))
1742                .ok_or("cannot pass duplicate boot devices")?;
1743
1744            order.push(default_order[default_pos].take().unwrap());
1745        }
1746
1747        order.extend(default_order.into_iter().flatten());
1748        assert_eq!(order.len(), 4);
1749
1750        Ok(Self(order.try_into().unwrap()))
1751    }
1752}
1753
1754#[derive(Copy, Clone, Debug, ValueEnum)]
1755pub enum UefiConsoleModeCli {
1756    Default,
1757    Com1,
1758    Com2,
1759    None,
1760}
1761
1762#[derive(Copy, Clone, Debug, Default, ValueEnum)]
1763pub enum EfiDiagnosticsLogLevelCli {
1764    #[default]
1765    Default,
1766    Info,
1767    Full,
1768}
1769
1770#[derive(Clone, Debug, PartialEq)]
1771pub struct PcieRootComplexCli {
1772    pub name: String,
1773    pub segment: u16,
1774    pub start_bus: u8,
1775    pub end_bus: u8,
1776    pub low_mmio: u32,
1777    pub high_mmio: u64,
1778}
1779
1780impl FromStr for PcieRootComplexCli {
1781    type Err = anyhow::Error;
1782
1783    fn from_str(s: &str) -> Result<Self, Self::Err> {
1784        const DEFAULT_PCIE_CRS_LOW_SIZE: u32 = 64 * 1024 * 1024; // 64M
1785        const DEFAULT_PCIE_CRS_HIGH_SIZE: u64 = 1024 * 1024 * 1024; // 1G
1786
1787        let mut opts = s.split(',');
1788        let name = opts.next().context("expected root complex name")?;
1789        if name.is_empty() {
1790            anyhow::bail!("must provide a root complex name");
1791        }
1792
1793        let mut segment = 0;
1794        let mut start_bus = 0;
1795        let mut end_bus = 255;
1796        let mut low_mmio = DEFAULT_PCIE_CRS_LOW_SIZE;
1797        let mut high_mmio = DEFAULT_PCIE_CRS_HIGH_SIZE;
1798        for opt in opts {
1799            let mut s = opt.split('=');
1800            let opt = s.next().context("expected option")?;
1801            match opt {
1802                "segment" => {
1803                    let seg_str = s.next().context("expected segment number")?;
1804                    segment = u16::from_str(seg_str).context("failed to parse segment number")?;
1805                }
1806                "start_bus" => {
1807                    let bus_str = s.next().context("expected start bus number")?;
1808                    start_bus =
1809                        u8::from_str(bus_str).context("failed to parse start bus number")?;
1810                }
1811                "end_bus" => {
1812                    let bus_str = s.next().context("expected end bus number")?;
1813                    end_bus = u8::from_str(bus_str).context("failed to parse end bus number")?;
1814                }
1815                "low_mmio" => {
1816                    let low_mmio_str = s.next().context("expected low MMIO size")?;
1817                    low_mmio = parse_memory(low_mmio_str)
1818                        .context("failed to parse low MMIO size")?
1819                        .try_into()?;
1820                }
1821                "high_mmio" => {
1822                    let high_mmio_str = s.next().context("expected high MMIO size")?;
1823                    high_mmio =
1824                        parse_memory(high_mmio_str).context("failed to parse high MMIO size")?;
1825                }
1826                opt => anyhow::bail!("unknown option: '{opt}'"),
1827            }
1828        }
1829
1830        if start_bus >= end_bus {
1831            anyhow::bail!("start_bus must be less than or equal to end_bus");
1832        }
1833
1834        Ok(PcieRootComplexCli {
1835            name: name.to_string(),
1836            segment,
1837            start_bus,
1838            end_bus,
1839            low_mmio,
1840            high_mmio,
1841        })
1842    }
1843}
1844
1845#[derive(Clone, Debug, PartialEq)]
1846pub struct PcieRootPortCli {
1847    pub root_complex_name: String,
1848    pub name: String,
1849    pub hotplug: bool,
1850}
1851
1852impl FromStr for PcieRootPortCli {
1853    type Err = anyhow::Error;
1854
1855    fn from_str(s: &str) -> Result<Self, Self::Err> {
1856        let mut opts = s.split(',');
1857        let names = opts.next().context("expected root port identifiers")?;
1858        if names.is_empty() {
1859            anyhow::bail!("must provide root port identifiers");
1860        }
1861
1862        let mut s = names.split(':');
1863        let rc_name = s.next().context("expected name of parent root complex")?;
1864        let rp_name = s.next().context("expected root port name")?;
1865
1866        if let Some(extra) = s.next() {
1867            anyhow::bail!("unexpected token: '{extra}'")
1868        }
1869
1870        let mut hotplug = false;
1871
1872        // Parse optional flags
1873        for opt in opts {
1874            match opt {
1875                "hotplug" => hotplug = true,
1876                _ => anyhow::bail!("unexpected option: '{opt}'"),
1877            }
1878        }
1879
1880        Ok(PcieRootPortCli {
1881            root_complex_name: rc_name.to_string(),
1882            name: rp_name.to_string(),
1883            hotplug,
1884        })
1885    }
1886}
1887
1888#[derive(Clone, Debug, PartialEq)]
1889pub struct GenericPcieSwitchCli {
1890    pub port_name: String,
1891    pub name: String,
1892    pub num_downstream_ports: u8,
1893    pub hotplug: bool,
1894}
1895
1896impl FromStr for GenericPcieSwitchCli {
1897    type Err = anyhow::Error;
1898
1899    fn from_str(s: &str) -> Result<Self, Self::Err> {
1900        let mut opts = s.split(',');
1901        let names = opts.next().context("expected switch identifiers")?;
1902        if names.is_empty() {
1903            anyhow::bail!("must provide switch identifiers");
1904        }
1905
1906        let mut s = names.split(':');
1907        let port_name = s.next().context("expected name of parent port")?;
1908        let switch_name = s.next().context("expected switch name")?;
1909
1910        if let Some(extra) = s.next() {
1911            anyhow::bail!("unexpected token: '{extra}'")
1912        }
1913
1914        let mut num_downstream_ports = 4u8; // Default value
1915        let mut hotplug = false;
1916
1917        for opt in opts {
1918            let mut kv = opt.split('=');
1919            let key = kv.next().context("expected option name")?;
1920
1921            match key {
1922                "num_downstream_ports" => {
1923                    let value = kv.next().context("expected option value")?;
1924                    if let Some(extra) = kv.next() {
1925                        anyhow::bail!("unexpected token: '{extra}'")
1926                    }
1927                    num_downstream_ports = value.parse().context("invalid num_downstream_ports")?;
1928                }
1929                "hotplug" => {
1930                    if kv.next().is_some() {
1931                        anyhow::bail!("hotplug option does not take a value")
1932                    }
1933                    hotplug = true;
1934                }
1935                _ => anyhow::bail!("unknown option: '{key}'"),
1936            }
1937        }
1938
1939        Ok(GenericPcieSwitchCli {
1940            port_name: port_name.to_string(),
1941            name: switch_name.to_string(),
1942            num_downstream_ports,
1943            hotplug,
1944        })
1945    }
1946}
1947
1948/// CLI configuration for a PCIe remote device.
1949#[derive(Clone, Debug, PartialEq)]
1950pub struct PcieRemoteCli {
1951    /// Name of the PCIe downstream port to attach to.
1952    pub port_name: String,
1953    /// TCP socket address for the remote simulator.
1954    pub socket_addr: Option<String>,
1955    /// Hardware unit identifier for plug request.
1956    pub hu: u16,
1957    /// Controller identifier for plug request.
1958    pub controller: u16,
1959}
1960
1961impl FromStr for PcieRemoteCli {
1962    type Err = anyhow::Error;
1963
1964    fn from_str(s: &str) -> Result<Self, Self::Err> {
1965        let mut opts = s.split(',');
1966        let port_name = opts.next().context("expected port name")?;
1967        if port_name.is_empty() {
1968            anyhow::bail!("must provide a port name");
1969        }
1970
1971        let mut socket_addr = None;
1972        let mut hu = 0u16;
1973        let mut controller = 0u16;
1974
1975        for opt in opts {
1976            let mut kv = opt.split('=');
1977            let key = kv.next().context("expected option name")?;
1978            let value = kv.next();
1979
1980            match key {
1981                "socket" => {
1982                    let addr = value.context("socket requires an address")?;
1983                    if let Some(extra) = kv.next() {
1984                        anyhow::bail!("unexpected token: '{extra}'")
1985                    }
1986                    if addr.is_empty() {
1987                        anyhow::bail!("socket address cannot be empty");
1988                    }
1989                    socket_addr = Some(addr.to_string());
1990                }
1991                "hu" => {
1992                    let val = value.context("hu requires a value")?;
1993                    if let Some(extra) = kv.next() {
1994                        anyhow::bail!("unexpected token: '{extra}'")
1995                    }
1996                    hu = val.parse().context("failed to parse hu")?;
1997                }
1998                "controller" => {
1999                    let val = value.context("controller requires a value")?;
2000                    if let Some(extra) = kv.next() {
2001                        anyhow::bail!("unexpected token: '{extra}'")
2002                    }
2003                    controller = val.parse().context("failed to parse controller")?;
2004                }
2005                _ => anyhow::bail!("unknown option: '{key}'"),
2006            }
2007        }
2008
2009        Ok(PcieRemoteCli {
2010            port_name: port_name.to_string(),
2011            socket_addr,
2012            hu,
2013            controller,
2014        })
2015    }
2016}
2017
2018/// CLI configuration for a VFIO-assigned PCI device.
2019#[cfg(target_os = "linux")]
2020#[derive(Clone, Debug)]
2021pub struct VfioDeviceCli {
2022    /// Name of the PCIe downstream port to attach to.
2023    pub port_name: String,
2024    /// PCI BDF address of the device on the host (e.g., "0000:01:00.0").
2025    pub pci_id: String,
2026}
2027
2028#[cfg(target_os = "linux")]
2029impl FromStr for VfioDeviceCli {
2030    type Err = anyhow::Error;
2031
2032    fn from_str(s: &str) -> Result<Self, Self::Err> {
2033        let (port_name, pci_id) = s
2034            .split_once(':')
2035            .context("expected <port_name>:<pci_bdf> (e.g., rp0:0000:01:00.0)")?;
2036
2037        if port_name.is_empty() {
2038            anyhow::bail!("port name cannot be empty");
2039        }
2040
2041        if pci_id.is_empty() {
2042            anyhow::bail!("PCI address cannot be empty");
2043        }
2044
2045        // Reject path separators to prevent sysfs path traversal via Path::join.
2046        if pci_id.contains('/') || pci_id.contains("..") {
2047            anyhow::bail!("PCI address must not contain path separators");
2048        }
2049
2050        Ok(VfioDeviceCli {
2051            port_name: port_name.to_string(),
2052            pci_id: pci_id.to_string(),
2053        })
2054    }
2055}
2056
2057/// Read a environment variable that may / may-not have a target-specific
2058/// prefix. e.g: `default_value_from_arch_env("FOO")` would first try and read
2059/// from `FOO`, and if that's not found, it will try `X86_64_FOO`.
2060///
2061/// Must return an `OsString`, in order to be compatible with `clap`'s
2062/// default_value code. As such - to encode the absence of the env-var, an empty
2063/// OsString is returned.
2064fn default_value_from_arch_env(name: &str) -> OsString {
2065    let prefix = if cfg!(guest_arch = "x86_64") {
2066        "X86_64"
2067    } else if cfg!(guest_arch = "aarch64") {
2068        "AARCH64"
2069    } else {
2070        return Default::default();
2071    };
2072    let prefixed = format!("{}_{}", prefix, name);
2073    std::env::var_os(name)
2074        .or_else(|| std::env::var_os(prefixed))
2075        .unwrap_or_default()
2076}
2077
2078/// Workaround to use `Option<PathBuf>` alongside [`default_value_from_arch_env`]
2079#[derive(Clone)]
2080pub struct OptionalPathBuf(pub Option<PathBuf>);
2081
2082impl From<&std::ffi::OsStr> for OptionalPathBuf {
2083    fn from(s: &std::ffi::OsStr) -> Self {
2084        OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
2085    }
2086}
2087
2088#[cfg(target_os = "linux")]
2089#[derive(Clone)]
2090pub enum VhostUserDeviceTypeCli {
2091    /// Block device — config from backend via GET_CONFIG, with num_queues
2092    /// patched by the frontend.
2093    Blk {
2094        num_queues: Option<u16>,
2095        queue_size: Option<u16>,
2096    },
2097    /// Filesystem device — frontend-owned config with mount tag.
2098    Fs {
2099        tag: String,
2100        num_queues: Option<u16>,
2101        queue_size: Option<u16>,
2102    },
2103    /// Generic device identified by numeric virtio device ID.
2104    Other {
2105        device_id: u16,
2106        queue_sizes: Vec<u16>,
2107    },
2108}
2109
2110#[cfg(target_os = "linux")]
2111#[derive(Clone)]
2112pub struct VhostUserCli {
2113    pub socket_path: String,
2114    pub device_type: VhostUserDeviceTypeCli,
2115    pub pcie_port: Option<String>,
2116}
2117
2118/// Split a string on commas, but not inside `[…]` brackets.
2119///
2120/// Returns an error on mismatched brackets (unmatched `]` or unclosed `[`).
2121#[cfg(target_os = "linux")]
2122fn split_respecting_brackets(s: &str) -> anyhow::Result<Vec<&str>> {
2123    let mut result = Vec::new();
2124    let mut start = 0;
2125    let mut depth: i32 = 0;
2126    for (i, c) in s.char_indices() {
2127        match c {
2128            '[' => depth += 1,
2129            ']' => {
2130                depth -= 1;
2131                anyhow::ensure!(depth >= 0, "unmatched ']' in option string");
2132            }
2133            ',' if depth == 0 => {
2134                result.push(&s[start..i]);
2135                start = i + 1;
2136            }
2137            _ => {}
2138        }
2139    }
2140    anyhow::ensure!(depth == 0, "unclosed '[' in option string");
2141    result.push(&s[start..]);
2142    Ok(result)
2143}
2144
2145#[cfg(target_os = "linux")]
2146impl FromStr for VhostUserCli {
2147    type Err = anyhow::Error;
2148
2149    fn from_str(s: &str) -> anyhow::Result<Self> {
2150        // Split on commas, but not inside brackets (for queue_sizes=[N,N]).
2151        let parts = split_respecting_brackets(s)?;
2152        let mut parts_iter = parts.into_iter();
2153        let socket_path = parts_iter
2154            .next()
2155            .context("missing socket path")?
2156            .to_string();
2157
2158        let mut device_id: Option<u16> = None;
2159        let mut tag: Option<String> = None;
2160        let mut pcie_port: Option<String> = None;
2161        let mut type_name = None;
2162        let mut num_queues: Option<u16> = None;
2163        let mut queue_size: Option<u16> = None;
2164        let mut queue_sizes: Option<Vec<u16>> = None;
2165        for opt in parts_iter {
2166            let (key, val) = opt.split_once('=').context("expected key=value option")?;
2167            match key {
2168                "type" => {
2169                    type_name = Some(val);
2170                }
2171                "device_id" => {
2172                    device_id = Some(val.parse().context("invalid device_id")?);
2173                }
2174                "tag" => {
2175                    tag = Some(val.to_string());
2176                }
2177                "pcie_port" => {
2178                    pcie_port = Some(val.to_string());
2179                }
2180                "num_queues" => {
2181                    num_queues = Some(val.parse().context("invalid num_queues")?);
2182                }
2183                "queue_size" => {
2184                    queue_size = Some(val.parse().context("invalid queue_size")?);
2185                }
2186                "queue_sizes" => {
2187                    // Parse bracket-delimited comma-separated list: [N,N,N]
2188                    let trimmed = val
2189                        .strip_prefix('[')
2190                        .and_then(|v| v.strip_suffix(']'))
2191                        .context("queue_sizes must be bracketed: [N,N,N]")?;
2192                    let sizes: Vec<u16> = trimmed
2193                        .split(',')
2194                        .map(|s| s.parse().context("invalid queue size in queue_sizes"))
2195                        .collect::<anyhow::Result<_>>()?;
2196                    anyhow::ensure!(!sizes.is_empty(), "queue_sizes must be non-empty");
2197                    queue_sizes = Some(sizes);
2198                }
2199                other => anyhow::bail!("unknown vhost-user option: '{other}'"),
2200            }
2201        }
2202
2203        if type_name.is_some() == device_id.is_some() {
2204            anyhow::bail!("must specify type=<name> or device_id=<N>");
2205        }
2206
2207        // Build the typed device variant.
2208        let device_type = match type_name {
2209            Some("fs") => {
2210                let tag = tag.take().context("type=fs requires tag=<name>")?;
2211                VhostUserDeviceTypeCli::Fs {
2212                    tag,
2213                    num_queues: num_queues.take(),
2214                    queue_size: queue_size.take(),
2215                }
2216            }
2217            Some("blk") => VhostUserDeviceTypeCli::Blk {
2218                num_queues: num_queues.take(),
2219                queue_size: queue_size.take(),
2220            },
2221            Some(ty) => anyhow::bail!("unknown vhost-user device type: '{ty}'"),
2222            None => {
2223                let queue_sizes = queue_sizes
2224                    .take()
2225                    .context("device_id= requires queue_sizes=[N,N,...]")?;
2226                VhostUserDeviceTypeCli::Other {
2227                    device_id: device_id.unwrap(),
2228                    queue_sizes,
2229                }
2230            }
2231        };
2232
2233        if tag.is_some() {
2234            anyhow::bail!("tag= is only valid for type=fs");
2235        }
2236        if queue_sizes.is_some() {
2237            anyhow::bail!("queue_sizes= is only valid for device_id=");
2238        }
2239        if num_queues.is_some() || queue_size.is_some() {
2240            anyhow::bail!(
2241                "num_queues= and queue_size= are not valid for device_id=; use queue_sizes="
2242            );
2243        }
2244
2245        Ok(VhostUserCli {
2246            socket_path,
2247            device_type,
2248            pcie_port,
2249        })
2250    }
2251}
2252
2253#[cfg(test)]
2254mod tests {
2255    use super::*;
2256
2257    use std::path::Path;
2258
2259    #[test]
2260    fn test_parse_file_opts() {
2261        // file: prefix with create
2262        let disk = DiskCliKind::from_str("file:test.vhd;create=1G").unwrap();
2263        assert!(matches!(
2264            &disk,
2265            DiskCliKind::File { path, create_with_len: Some(len), direct: false }
2266                if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
2267        ));
2268
2269        // bare path with create (no file: prefix)
2270        let disk = DiskCliKind::from_str("test.vhd;create=1G").unwrap();
2271        assert!(matches!(
2272            &disk,
2273            DiskCliKind::File { path, create_with_len: Some(len), direct: false }
2274                if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
2275        ));
2276
2277        // direct flag
2278        let disk = DiskCliKind::from_str("file:/dev/sdb;direct").unwrap();
2279        assert!(matches!(
2280            &disk,
2281            DiskCliKind::File { path, create_with_len: None, direct: true }
2282                if path == Path::new("/dev/sdb")
2283        ));
2284
2285        // direct + create in either order
2286        let disk = DiskCliKind::from_str("file:disk.img;direct;create=1G").unwrap();
2287        assert!(matches!(
2288            &disk,
2289            DiskCliKind::File { path, create_with_len: Some(len), direct: true }
2290                if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
2291        ));
2292
2293        let disk = DiskCliKind::from_str("file:disk.img;create=1G;direct").unwrap();
2294        assert!(matches!(
2295            &disk,
2296            DiskCliKind::File { path, create_with_len: Some(len), direct: true }
2297                if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
2298        ));
2299
2300        // plain path, no options
2301        let disk = DiskCliKind::from_str("file:disk.img").unwrap();
2302        assert!(matches!(
2303            &disk,
2304            DiskCliKind::File { path, create_with_len: None, direct: false }
2305                if path == Path::new("disk.img")
2306        ));
2307
2308        // invalid option rejected
2309        assert!(DiskCliKind::from_str("file:disk.img;bogus").is_err());
2310
2311        // direct rejected for sql disks
2312        assert!(DiskCliKind::from_str("sql:db.sqlite;direct").is_err());
2313    }
2314
2315    #[test]
2316    fn test_parse_memory_disk() {
2317        let s = "mem:1G";
2318        let disk = DiskCliKind::from_str(s).unwrap();
2319        match disk {
2320            DiskCliKind::Memory(size) => {
2321                assert_eq!(size, 1024 * 1024 * 1024); // 1G
2322            }
2323            _ => panic!("Expected Memory variant"),
2324        }
2325    }
2326
2327    #[test]
2328    fn test_parse_pcie_disk() {
2329        assert_eq!(
2330            DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
2331            Some("p0".to_string())
2332        );
2333        assert_eq!(
2334            DiskCli::from_str("file:path.vhdx,pcie_port=p0")
2335                .unwrap()
2336                .pcie_port,
2337            Some("p0".to_string())
2338        );
2339        assert_eq!(
2340            DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
2341                .unwrap()
2342                .pcie_port,
2343            Some("p0".to_string())
2344        );
2345
2346        // Missing port name
2347        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
2348
2349        // Incompatible with various other disk fields
2350        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
2351        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
2352        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
2353    }
2354
2355    #[test]
2356    fn test_parse_memory_diff_disk() {
2357        let s = "memdiff:file:base.img";
2358        let disk = DiskCliKind::from_str(s).unwrap();
2359        match disk {
2360            DiskCliKind::MemoryDiff(inner) => match *inner {
2361                DiskCliKind::File {
2362                    path,
2363                    create_with_len,
2364                    ..
2365                } => {
2366                    assert_eq!(path, PathBuf::from("base.img"));
2367                    assert_eq!(create_with_len, None);
2368                }
2369                _ => panic!("Expected File variant inside MemoryDiff"),
2370            },
2371            _ => panic!("Expected MemoryDiff variant"),
2372        }
2373    }
2374
2375    #[test]
2376    fn test_parse_sqlite_disk() {
2377        let s = "sql:db.sqlite;create=2G";
2378        let disk = DiskCliKind::from_str(s).unwrap();
2379        match disk {
2380            DiskCliKind::Sqlite {
2381                path,
2382                create_with_len,
2383            } => {
2384                assert_eq!(path, PathBuf::from("db.sqlite"));
2385                assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
2386            }
2387            _ => panic!("Expected Sqlite variant"),
2388        }
2389
2390        // Test without create option
2391        let s = "sql:db.sqlite";
2392        let disk = DiskCliKind::from_str(s).unwrap();
2393        match disk {
2394            DiskCliKind::Sqlite {
2395                path,
2396                create_with_len,
2397            } => {
2398                assert_eq!(path, PathBuf::from("db.sqlite"));
2399                assert_eq!(create_with_len, None);
2400            }
2401            _ => panic!("Expected Sqlite variant"),
2402        }
2403    }
2404
2405    #[test]
2406    fn test_parse_sqlite_diff_disk() {
2407        // Test with create option
2408        let s = "sqldiff:diff.sqlite;create:file:base.img";
2409        let disk = DiskCliKind::from_str(s).unwrap();
2410        match disk {
2411            DiskCliKind::SqliteDiff { path, create, disk } => {
2412                assert_eq!(path, PathBuf::from("diff.sqlite"));
2413                assert!(create);
2414                match *disk {
2415                    DiskCliKind::File {
2416                        path,
2417                        create_with_len,
2418                        ..
2419                    } => {
2420                        assert_eq!(path, PathBuf::from("base.img"));
2421                        assert_eq!(create_with_len, None);
2422                    }
2423                    _ => panic!("Expected File variant inside SqliteDiff"),
2424                }
2425            }
2426            _ => panic!("Expected SqliteDiff variant"),
2427        }
2428
2429        // Test without create option
2430        let s = "sqldiff:diff.sqlite:file:base.img";
2431        let disk = DiskCliKind::from_str(s).unwrap();
2432        match disk {
2433            DiskCliKind::SqliteDiff { path, create, disk } => {
2434                assert_eq!(path, PathBuf::from("diff.sqlite"));
2435                assert!(!create);
2436                match *disk {
2437                    DiskCliKind::File {
2438                        path,
2439                        create_with_len,
2440                        ..
2441                    } => {
2442                        assert_eq!(path, PathBuf::from("base.img"));
2443                        assert_eq!(create_with_len, None);
2444                    }
2445                    _ => panic!("Expected File variant inside SqliteDiff"),
2446                }
2447            }
2448            _ => panic!("Expected SqliteDiff variant"),
2449        }
2450    }
2451
2452    #[test]
2453    fn test_parse_autocache_sqlite_disk() {
2454        // Test with cache path provided
2455        let disk =
2456            DiskCliKind::parse_autocache(":file:disk.vhd", Ok("/tmp/cache".to_string())).unwrap();
2457        assert!(matches!(
2458            disk,
2459            DiskCliKind::AutoCacheSqlite {
2460                cache_path,
2461                key,
2462                disk: _disk,
2463            } if cache_path == "/tmp/cache" && key.is_none()
2464        ));
2465
2466        // Test with key
2467        let disk =
2468            DiskCliKind::parse_autocache("mykey:file:disk.vhd", Ok("/tmp/cache".to_string()))
2469                .unwrap();
2470        assert!(matches!(
2471            disk,
2472            DiskCliKind::AutoCacheSqlite {
2473                cache_path,
2474                key: Some(key),
2475                disk: _disk,
2476            } if cache_path == "/tmp/cache" && key == "mykey"
2477        ));
2478
2479        // Test without cache path
2480        assert!(
2481            DiskCliKind::parse_autocache(":file:disk.vhd", Err(std::env::VarError::NotPresent),)
2482                .is_err()
2483        );
2484    }
2485
2486    #[test]
2487    fn test_parse_disk_errors() {
2488        assert!(DiskCliKind::from_str("invalid:").is_err());
2489        assert!(DiskCliKind::from_str("memory:extra").is_err());
2490
2491        // Test sqlite: without environment variable
2492        assert!(DiskCliKind::from_str("sqlite:").is_err());
2493    }
2494
2495    #[test]
2496    fn test_parse_errors() {
2497        // Invalid memory size
2498        assert!(DiskCliKind::from_str("mem:invalid").is_err());
2499
2500        // Invalid syntax for SQLiteDiff
2501        assert!(DiskCliKind::from_str("sqldiff:path").is_err());
2502
2503        // Missing OPENVMM_AUTO_CACHE_PATH for AutoCacheSqlite
2504        assert!(
2505            DiskCliKind::parse_autocache("key:file:disk.vhd", Err(std::env::VarError::NotPresent),)
2506                .is_err()
2507        );
2508
2509        // Invalid blob kind
2510        assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
2511
2512        // Invalid cipher
2513        assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
2514
2515        // Invalid format for crypt (missing parts)
2516        assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
2517
2518        // Invalid disk kind
2519        assert!(DiskCliKind::from_str("invalid:path").is_err());
2520
2521        // Missing create size
2522        assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
2523    }
2524
2525    #[test]
2526    fn test_fs_args_from_str() {
2527        let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
2528        assert_eq!(args.tag, "tag1");
2529        assert_eq!(args.path, "/path/to/fs");
2530
2531        // Test error cases
2532        assert!(FsArgs::from_str("tag1").is_err());
2533        assert!(FsArgs::from_str("tag1,/path,extra").is_err());
2534    }
2535
2536    #[test]
2537    fn test_fs_args_with_options_from_str() {
2538        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
2539        assert_eq!(args.tag, "tag1");
2540        assert_eq!(args.path, "/path/to/fs");
2541        assert_eq!(args.options, "opt1;opt2");
2542
2543        // Test without options
2544        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
2545        assert_eq!(args.tag, "tag1");
2546        assert_eq!(args.path, "/path/to/fs");
2547        assert_eq!(args.options, "");
2548
2549        // Test error case
2550        assert!(FsArgsWithOptions::from_str("tag1").is_err());
2551    }
2552
2553    #[test]
2554    fn test_serial_config_from_str() {
2555        assert_eq!(
2556            SerialConfigCli::from_str("none").unwrap(),
2557            SerialConfigCli::None
2558        );
2559        assert_eq!(
2560            SerialConfigCli::from_str("console").unwrap(),
2561            SerialConfigCli::Console
2562        );
2563        assert_eq!(
2564            SerialConfigCli::from_str("stderr").unwrap(),
2565            SerialConfigCli::Stderr
2566        );
2567
2568        // Test file config
2569        let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
2570        if let SerialConfigCli::File(path) = file_config {
2571            assert_eq!(path.to_str().unwrap(), "/path/to/file");
2572        } else {
2573            panic!("Expected File variant");
2574        }
2575
2576        // Test term config with name, but no specific path
2577        match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
2578            SerialConfigCli::NewConsole(None, Some(name)) => {
2579                assert_eq!(name, "MyTerm");
2580            }
2581            _ => panic!("Expected NewConsole variant with name"),
2582        }
2583
2584        // Test term config without name, but no specific path
2585        match SerialConfigCli::from_str("term").unwrap() {
2586            SerialConfigCli::NewConsole(None, None) => (),
2587            _ => panic!("Expected NewConsole variant without name"),
2588        }
2589
2590        // Test term config with name
2591        match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
2592            SerialConfigCli::NewConsole(Some(path), Some(name)) => {
2593                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
2594                assert_eq!(name, "MyTerm");
2595            }
2596            _ => panic!("Expected NewConsole variant with name"),
2597        }
2598
2599        // Test term config without name
2600        match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
2601            SerialConfigCli::NewConsole(Some(path), None) => {
2602                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
2603            }
2604            _ => panic!("Expected NewConsole variant without name"),
2605        }
2606
2607        // Test TCP config
2608        match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
2609            SerialConfigCli::Tcp(addr) => {
2610                assert_eq!(addr.to_string(), "127.0.0.1:1234");
2611            }
2612            _ => panic!("Expected Tcp variant"),
2613        }
2614
2615        // Test pipe config
2616        match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
2617            SerialConfigCli::Pipe(path) => {
2618                assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
2619            }
2620            _ => panic!("Expected Pipe variant"),
2621        }
2622
2623        // Test error cases
2624        assert!(SerialConfigCli::from_str("").is_err());
2625        assert!(SerialConfigCli::from_str("unknown").is_err());
2626        assert!(SerialConfigCli::from_str("file").is_err());
2627        assert!(SerialConfigCli::from_str("listen").is_err());
2628    }
2629
2630    #[test]
2631    fn test_endpoint_config_from_str() {
2632        // Test none
2633        assert!(matches!(
2634            EndpointConfigCli::from_str("none").unwrap(),
2635            EndpointConfigCli::None
2636        ));
2637
2638        // Test consomme without cidr
2639        match EndpointConfigCli::from_str("consomme").unwrap() {
2640            EndpointConfigCli::Consomme { cidr: None } => (),
2641            _ => panic!("Expected Consomme variant without cidr"),
2642        }
2643
2644        // Test consomme with cidr
2645        match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
2646            EndpointConfigCli::Consomme { cidr: Some(cidr) } => {
2647                assert_eq!(cidr, "192.168.0.0/24");
2648            }
2649            _ => panic!("Expected Consomme variant with cidr"),
2650        }
2651
2652        // Test dio without id
2653        match EndpointConfigCli::from_str("dio").unwrap() {
2654            EndpointConfigCli::Dio { id: None } => (),
2655            _ => panic!("Expected Dio variant without id"),
2656        }
2657
2658        // Test dio with id
2659        match EndpointConfigCli::from_str("dio:test_id").unwrap() {
2660            EndpointConfigCli::Dio { id: Some(id) } => {
2661                assert_eq!(id, "test_id");
2662            }
2663            _ => panic!("Expected Dio variant with id"),
2664        }
2665
2666        // Test tap
2667        match EndpointConfigCli::from_str("tap:tap0").unwrap() {
2668            EndpointConfigCli::Tap { name } => {
2669                assert_eq!(name, "tap0");
2670            }
2671            _ => panic!("Expected Tap variant"),
2672        }
2673
2674        // Test error case
2675        assert!(EndpointConfigCli::from_str("invalid").is_err());
2676    }
2677
2678    #[test]
2679    fn test_nic_config_from_str() {
2680        use openvmm_defs::config::DeviceVtl;
2681
2682        // Test basic endpoint
2683        let config = NicConfigCli::from_str("none").unwrap();
2684        assert_eq!(config.vtl, DeviceVtl::Vtl0);
2685        assert!(config.max_queues.is_none());
2686        assert!(!config.underhill);
2687        assert!(config.pcie_port.is_none());
2688        assert!(matches!(config.endpoint, EndpointConfigCli::None));
2689
2690        // Test with vtl2
2691        let config = NicConfigCli::from_str("vtl2:none").unwrap();
2692        assert_eq!(config.vtl, DeviceVtl::Vtl2);
2693        assert!(config.pcie_port.is_none());
2694        assert!(matches!(config.endpoint, EndpointConfigCli::None));
2695
2696        // Test with queues
2697        let config = NicConfigCli::from_str("queues=4:none").unwrap();
2698        assert_eq!(config.max_queues, Some(4));
2699        assert!(config.pcie_port.is_none());
2700        assert!(matches!(config.endpoint, EndpointConfigCli::None));
2701
2702        // Test with underhill
2703        let config = NicConfigCli::from_str("uh:none").unwrap();
2704        assert!(config.underhill);
2705        assert!(config.pcie_port.is_none());
2706        assert!(matches!(config.endpoint, EndpointConfigCli::None));
2707
2708        // Test with pcie_port
2709        let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
2710        assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
2711        assert!(matches!(config.endpoint, EndpointConfigCli::None));
2712
2713        // Test error cases
2714        assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
2715        assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); // uh incompatible with vtl2
2716        assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
2717        assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
2718        assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
2719        assert!(NicConfigCli::from_str("pcie_port:none").is_err());
2720    }
2721
2722    #[test]
2723    fn test_parse_pcie_port_prefix() {
2724        // Successful prefix parsing
2725        let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
2726        assert_eq!(port.unwrap(), "rp0");
2727        assert_eq!(rest, "tag,path");
2728
2729        // No prefix
2730        let (port, rest) = parse_pcie_port_prefix("tag,path");
2731        assert!(port.is_none());
2732        assert_eq!(rest, "tag,path");
2733
2734        // Empty port name — not parsed as a prefix
2735        let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
2736        assert!(port.is_none());
2737        assert_eq!(rest, "pcie_port=:tag,path");
2738
2739        // Missing colon — not parsed as a prefix
2740        let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
2741        assert!(port.is_none());
2742        assert_eq!(rest, "pcie_port=rp0");
2743    }
2744
2745    #[test]
2746    fn test_fs_args_pcie_port() {
2747        // Without pcie_port
2748        let args = FsArgs::from_str("myfs,/path").unwrap();
2749        assert_eq!(args.tag, "myfs");
2750        assert_eq!(args.path, "/path");
2751        assert!(args.pcie_port.is_none());
2752
2753        // With pcie_port
2754        let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
2755        assert_eq!(args.pcie_port.unwrap(), "rp0");
2756        assert_eq!(args.tag, "myfs");
2757        assert_eq!(args.path, "/path");
2758
2759        // Error: wrong number of fields
2760        assert!(FsArgs::from_str("myfs").is_err());
2761        assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
2762    }
2763
2764    #[test]
2765    fn test_fs_args_with_options_pcie_port() {
2766        // Without pcie_port
2767        let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
2768        assert_eq!(args.tag, "myfs");
2769        assert_eq!(args.path, "/path");
2770        assert_eq!(args.options, "uid=1000");
2771        assert!(args.pcie_port.is_none());
2772
2773        // With pcie_port
2774        let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
2775        assert_eq!(args.pcie_port.unwrap(), "rp0");
2776        assert_eq!(args.tag, "myfs");
2777        assert_eq!(args.path, "/path");
2778        assert_eq!(args.options, "uid=1000");
2779
2780        // Error: missing path
2781        assert!(FsArgsWithOptions::from_str("myfs").is_err());
2782    }
2783
2784    #[test]
2785    fn test_virtio_pmem_args_pcie_port() {
2786        // Without pcie_port
2787        let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
2788        assert_eq!(args.path, "/path/to/file");
2789        assert!(args.pcie_port.is_none());
2790
2791        // With pcie_port
2792        let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
2793        assert_eq!(args.pcie_port.unwrap(), "rp0");
2794        assert_eq!(args.path, "/path/to/file");
2795
2796        // Error: empty path
2797        assert!(VirtioPmemArgs::from_str("").is_err());
2798        assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
2799    }
2800
2801    #[test]
2802    fn test_smt_config_from_str() {
2803        assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
2804        assert_eq!(
2805            SmtConfigCli::from_str("force").unwrap(),
2806            SmtConfigCli::Force
2807        );
2808        assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
2809
2810        // Test error cases
2811        assert!(SmtConfigCli::from_str("invalid").is_err());
2812        assert!(SmtConfigCli::from_str("").is_err());
2813    }
2814
2815    #[test]
2816    fn test_pcat_boot_order_from_str() {
2817        // Test single device
2818        let order = PcatBootOrderCli::from_str("optical").unwrap();
2819        assert_eq!(order.0[0], PcatBootDevice::Optical);
2820
2821        // Test multiple devices
2822        let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
2823        assert_eq!(order.0[0], PcatBootDevice::HardDrive);
2824        assert_eq!(order.0[1], PcatBootDevice::Network);
2825
2826        // Test error cases
2827        assert!(PcatBootOrderCli::from_str("invalid").is_err());
2828        assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); // duplicate device
2829    }
2830
2831    #[test]
2832    fn test_floppy_disk_from_str() {
2833        // Test basic disk
2834        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
2835        assert!(!disk.read_only);
2836        match disk.kind {
2837            DiskCliKind::File {
2838                path,
2839                create_with_len,
2840                ..
2841            } => {
2842                assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
2843                assert_eq!(create_with_len, None);
2844            }
2845            _ => panic!("Expected File variant"),
2846        }
2847
2848        // Test with read-only flag
2849        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
2850        assert!(disk.read_only);
2851
2852        // Test error cases
2853        assert!(FloppyDiskCli::from_str("").is_err());
2854        assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
2855    }
2856
2857    #[test]
2858    fn test_pcie_root_complex_from_str() {
2859        const ONE_MB: u64 = 1024 * 1024;
2860        const ONE_GB: u64 = 1024 * ONE_MB;
2861
2862        const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
2863        const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
2864
2865        assert_eq!(
2866            PcieRootComplexCli::from_str("rc0").unwrap(),
2867            PcieRootComplexCli {
2868                name: "rc0".to_string(),
2869                segment: 0,
2870                start_bus: 0,
2871                end_bus: 255,
2872                low_mmio: DEFAULT_LOW_MMIO,
2873                high_mmio: DEFAULT_HIGH_MMIO,
2874            }
2875        );
2876
2877        assert_eq!(
2878            PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
2879            PcieRootComplexCli {
2880                name: "rc1".to_string(),
2881                segment: 1,
2882                start_bus: 0,
2883                end_bus: 255,
2884                low_mmio: DEFAULT_LOW_MMIO,
2885                high_mmio: DEFAULT_HIGH_MMIO,
2886            }
2887        );
2888
2889        assert_eq!(
2890            PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
2891            PcieRootComplexCli {
2892                name: "rc2".to_string(),
2893                segment: 0,
2894                start_bus: 32,
2895                end_bus: 255,
2896                low_mmio: DEFAULT_LOW_MMIO,
2897                high_mmio: DEFAULT_HIGH_MMIO,
2898            }
2899        );
2900
2901        assert_eq!(
2902            PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
2903            PcieRootComplexCli {
2904                name: "rc3".to_string(),
2905                segment: 0,
2906                start_bus: 0,
2907                end_bus: 31,
2908                low_mmio: DEFAULT_LOW_MMIO,
2909                high_mmio: DEFAULT_HIGH_MMIO,
2910            }
2911        );
2912
2913        assert_eq!(
2914            PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
2915            PcieRootComplexCli {
2916                name: "rc4".to_string(),
2917                segment: 0,
2918                start_bus: 32,
2919                end_bus: 127,
2920                low_mmio: DEFAULT_LOW_MMIO,
2921                high_mmio: 2 * ONE_GB,
2922            }
2923        );
2924
2925        assert_eq!(
2926            PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
2927            PcieRootComplexCli {
2928                name: "rc5".to_string(),
2929                segment: 2,
2930                start_bus: 32,
2931                end_bus: 127,
2932                low_mmio: DEFAULT_LOW_MMIO,
2933                high_mmio: DEFAULT_HIGH_MMIO,
2934            }
2935        );
2936
2937        assert_eq!(
2938            PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
2939            PcieRootComplexCli {
2940                name: "rc6".to_string(),
2941                segment: 0,
2942                start_bus: 0,
2943                end_bus: 255,
2944                low_mmio: ONE_MB as u32,
2945                high_mmio: 64 * ONE_GB,
2946            }
2947        );
2948
2949        // Error cases
2950        assert!(PcieRootComplexCli::from_str("").is_err());
2951        assert!(PcieRootComplexCli::from_str("poorly,").is_err());
2952        assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
2953        assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
2954        assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
2955        assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
2956        assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
2957        assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
2958        assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
2959        assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
2960        assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
2961    }
2962
2963    #[test]
2964    fn test_pcie_root_port_from_str() {
2965        assert_eq!(
2966            PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
2967            PcieRootPortCli {
2968                root_complex_name: "rc0".to_string(),
2969                name: "rc0rp0".to_string(),
2970                hotplug: false,
2971            }
2972        );
2973
2974        assert_eq!(
2975            PcieRootPortCli::from_str("my_rc:port2").unwrap(),
2976            PcieRootPortCli {
2977                root_complex_name: "my_rc".to_string(),
2978                name: "port2".to_string(),
2979                hotplug: false,
2980            }
2981        );
2982
2983        // Test with hotplug flag
2984        assert_eq!(
2985            PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
2986            PcieRootPortCli {
2987                root_complex_name: "my_rc".to_string(),
2988                name: "port2".to_string(),
2989                hotplug: true,
2990            }
2991        );
2992
2993        // Error cases
2994        assert!(PcieRootPortCli::from_str("").is_err());
2995        assert!(PcieRootPortCli::from_str("rp0").is_err());
2996        assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
2997        assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
2998        assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
2999    }
3000
3001    #[test]
3002    fn test_pcie_switch_from_str() {
3003        assert_eq!(
3004            GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
3005            GenericPcieSwitchCli {
3006                port_name: "rp0".to_string(),
3007                name: "switch0".to_string(),
3008                num_downstream_ports: 4,
3009                hotplug: false,
3010            }
3011        );
3012
3013        assert_eq!(
3014            GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
3015            GenericPcieSwitchCli {
3016                port_name: "port1".to_string(),
3017                name: "my_switch".to_string(),
3018                num_downstream_ports: 4,
3019                hotplug: false,
3020            }
3021        );
3022
3023        assert_eq!(
3024            GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
3025            GenericPcieSwitchCli {
3026                port_name: "rp2".to_string(),
3027                name: "sw".to_string(),
3028                num_downstream_ports: 8,
3029                hotplug: false,
3030            }
3031        );
3032
3033        // Test hierarchical connections
3034        assert_eq!(
3035            GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
3036            GenericPcieSwitchCli {
3037                port_name: "switch0-downstream-1".to_string(),
3038                name: "child_switch".to_string(),
3039                num_downstream_ports: 4,
3040                hotplug: false,
3041            }
3042        );
3043
3044        // Test hotplug flag
3045        assert_eq!(
3046            GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
3047            GenericPcieSwitchCli {
3048                port_name: "rp0".to_string(),
3049                name: "switch0".to_string(),
3050                num_downstream_ports: 4,
3051                hotplug: true,
3052            }
3053        );
3054
3055        // Test hotplug with num_downstream_ports
3056        assert_eq!(
3057            GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
3058            GenericPcieSwitchCli {
3059                port_name: "rp0".to_string(),
3060                name: "switch0".to_string(),
3061                num_downstream_ports: 8,
3062                hotplug: true,
3063            }
3064        );
3065
3066        // Error cases
3067        assert!(GenericPcieSwitchCli::from_str("").is_err());
3068        assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
3069        assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
3070        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
3071        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
3072        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
3073        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
3074    }
3075
3076    #[test]
3077    fn test_pcie_remote_from_str() {
3078        // Basic port name only
3079        assert_eq!(
3080            PcieRemoteCli::from_str("rc0rp0").unwrap(),
3081            PcieRemoteCli {
3082                port_name: "rc0rp0".to_string(),
3083                socket_addr: None,
3084                hu: 0,
3085                controller: 0,
3086            }
3087        );
3088
3089        // With socket address
3090        assert_eq!(
3091            PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
3092            PcieRemoteCli {
3093                port_name: "rc0rp0".to_string(),
3094                socket_addr: Some("localhost:22567".to_string()),
3095                hu: 0,
3096                controller: 0,
3097            }
3098        );
3099
3100        // With all options
3101        assert_eq!(
3102            PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
3103            PcieRemoteCli {
3104                port_name: "myport".to_string(),
3105                socket_addr: Some("localhost:22568".to_string()),
3106                hu: 1,
3107                controller: 2,
3108            }
3109        );
3110
3111        // Only hu and controller
3112        assert_eq!(
3113            PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
3114            PcieRemoteCli {
3115                port_name: "port0".to_string(),
3116                socket_addr: None,
3117                hu: 5,
3118                controller: 3,
3119            }
3120        );
3121
3122        // Error cases
3123        assert!(PcieRemoteCli::from_str("").is_err());
3124        assert!(PcieRemoteCli::from_str("port,socket=").is_err());
3125        assert!(PcieRemoteCli::from_str("port,hu=").is_err());
3126        assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
3127        assert!(PcieRemoteCli::from_str("port,controller=").is_err());
3128        assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
3129        assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
3130    }
3131
3132    #[test]
3133    fn test_pidfile_option_parsed() {
3134        let opt = Options::try_parse_from(["openvmm", "--pidfile", "/tmp/test.pid"]).unwrap();
3135        assert_eq!(opt.pidfile, Some(PathBuf::from("/tmp/test.pid")));
3136    }
3137}