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