openvmm_entry/
cli_args.rs

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