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