underhill_core/
options.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! CLI argument parsing for the underhill core process.
5
6#![warn(missing_docs)]
7
8use anyhow::Context;
9use anyhow::bail;
10use inspect::Inspect;
11use mesh::MeshPayload;
12use std::collections::BTreeMap;
13use std::ffi::OsStr;
14use std::ffi::OsString;
15use std::path::PathBuf;
16use std::str::FromStr;
17
18#[derive(Clone, Debug, MeshPayload)]
19pub enum TestScenarioConfig {
20    SaveFail,
21    RestoreStuck,
22    SaveStuck,
23}
24
25impl FromStr for TestScenarioConfig {
26    type Err = anyhow::Error;
27
28    fn from_str(s: &str) -> Result<TestScenarioConfig, anyhow::Error> {
29        match s {
30            "SERVICING_SAVE_FAIL" => Ok(TestScenarioConfig::SaveFail),
31            "SERVICING_RESTORE_STUCK" => Ok(TestScenarioConfig::RestoreStuck),
32            "SERVICING_SAVE_STUCK" => Ok(TestScenarioConfig::SaveStuck),
33            _ => Err(anyhow::anyhow!("Invalid test config: {}", s)),
34        }
35    }
36}
37
38#[derive(Clone, Debug, MeshPayload)]
39pub enum GuestStateLifetimeCli {
40    Default,
41    ReprovisionOnFailure,
42    Reprovision,
43    Ephemeral,
44}
45
46impl FromStr for GuestStateLifetimeCli {
47    type Err = anyhow::Error;
48
49    fn from_str(s: &str) -> Result<GuestStateLifetimeCli, anyhow::Error> {
50        match s {
51            "DEFAULT" | "0" => Ok(GuestStateLifetimeCli::Default),
52            "REPROVISION_ON_FAILURE" | "1" => Ok(GuestStateLifetimeCli::ReprovisionOnFailure),
53            "REPROVISION" | "2" => Ok(GuestStateLifetimeCli::Reprovision),
54            "EPHEMERAL" | "3" => Ok(GuestStateLifetimeCli::Ephemeral),
55            _ => Err(anyhow::anyhow!("Invalid lifetime: {}", s)),
56        }
57    }
58}
59
60#[derive(Clone, Debug, MeshPayload)]
61pub enum GuestStateEncryptionPolicyCli {
62    Auto,
63    None,
64    GspById,
65    GspKey,
66}
67
68impl FromStr for GuestStateEncryptionPolicyCli {
69    type Err = anyhow::Error;
70
71    fn from_str(s: &str) -> Result<GuestStateEncryptionPolicyCli, anyhow::Error> {
72        match s {
73            "AUTO" | "0" => Ok(GuestStateEncryptionPolicyCli::Auto),
74            "NONE" | "1" => Ok(GuestStateEncryptionPolicyCli::None),
75            "GSP_BY_ID" | "2" => Ok(GuestStateEncryptionPolicyCli::GspById),
76            "GSP_KEY" | "3" => Ok(GuestStateEncryptionPolicyCli::GspKey),
77            _ => Err(anyhow::anyhow!("Invalid encryption policy: {}", s)),
78        }
79    }
80}
81
82#[derive(Clone, Debug, MeshPayload, Inspect)]
83pub enum KeepAliveConfig {
84    EnabledHostAndPrivatePoolPresent,
85    DisabledHostAndPrivatePoolPresent,
86    Disabled,
87}
88
89impl FromStr for KeepAliveConfig {
90    type Err = anyhow::Error;
91
92    fn from_str(s: &str) -> Result<KeepAliveConfig, anyhow::Error> {
93        match s.to_lowercase().as_str() {
94            "host,privatepool" => Ok(KeepAliveConfig::EnabledHostAndPrivatePoolPresent),
95            "nohost,privatepool" => Ok(KeepAliveConfig::DisabledHostAndPrivatePoolPresent),
96            "nohost,noprivatepool" => Ok(KeepAliveConfig::Disabled),
97            x if x.starts_with("disabled,") => Ok(KeepAliveConfig::Disabled),
98            _ => Err(anyhow::anyhow!("Invalid keepalive config: {}", s)),
99        }
100    }
101}
102
103impl KeepAliveConfig {
104    pub fn is_enabled(&self) -> bool {
105        matches!(self, KeepAliveConfig::EnabledHostAndPrivatePoolPresent)
106    }
107}
108
109// We've made our own parser here instead of using something like clap in order
110// to save on compiled file size. We don't need all the features a crate can provide.
111/// underhill core command-line and environment variable options.
112pub struct Options {
113    /// (OPENHCL_WAIT_FOR_START=1 | --wait-for-start)
114    ///  wait for a diagnostics start request before initializing and starting the VM
115    pub wait_for_start: bool,
116
117    /// (OPENHCL_SIGNAL_VTL0_STARTED=1)
118    /// immediately signal that VTL0 has started, before doing any
119    /// initialization. This allows VM boot to proceed even if initialization
120    /// may hang (e.g., because you specified OPENHCL_WAIT_FOR_START=1).
121    pub signal_vtl0_started: bool,
122
123    /// (OPENHCL_REFORMAT_VMGS=1 | --reformat-vmgs)
124    /// reformat the VMGS file on boot. useful for running potentially destructive VMGS tests.
125    pub reformat_vmgs: bool,
126
127    /// (OPENHCL_PID_FILE_PATH=/path/to/file | --pid /path/to/file)
128    /// write the PID to the specified path
129    pub pid: Option<PathBuf>,
130
131    /// (OPENHCL_VMBUS_MAX_VERSION=\<number\>)
132    /// limit the maximum protocol version allowed by vmbus; used for testing purposes
133    pub vmbus_max_version: Option<u32>,
134
135    /// (OPENHCL_VMBUS_ENABLE_MNF=1)
136    /// Enable handling of MNF in the Underhill vmbus server, instead of the host.
137    pub vmbus_enable_mnf: Option<bool>,
138
139    /// (OPENHCL_VMBUS_FORCE_CONFIDENTIAL_EXTERNAL_MEMORY=1)
140    /// Force the use of confidential external memory for all non-relay vmbus channels. For testing
141    /// purposes only.
142    ///
143    /// N.B.: Not all vmbus devices support this feature, so enabling it may cause failures.
144    pub vmbus_force_confidential_external_memory: bool,
145
146    /// (OPENHCL_VMBUS_CHANNEL_UNSTICK_DELAY_MS=\<number\>) (default: 100)
147    /// Delay before unsticking a vmbus channel after it has been opened, in milliseconds. Set to
148    /// zero to disable unsticking.
149    pub vmbus_channel_unstick_delay_ms: u64,
150
151    /// (OPENHCL_CMDLINE_APPEND=\<string\>)
152    /// Command line to append to VTL0, only used with direct boot.
153    pub cmdline_append: Option<String>,
154
155    /// (OPENHCL_VNC_PORT=\<number\> | --vnc-port \<number\>) (default: 3)
156    /// VNC (vsock) port number
157    pub vnc_port: u32,
158
159    /// (OPENHCL_GDBSTUB=1)
160    /// Enables the GDB stub for debugging the guest.
161    pub gdbstub: bool,
162
163    /// (OPENHCL_GDBSTUB_PORT=\<number\>) (default: 4)
164    /// GDB stub (vsock) port number.
165    pub gdbstub_port: u32,
166
167    /// (OPENHCL_VTL0_STARTS_PAUSED=1)
168    /// Start with VTL0 paused
169    pub vtl0_starts_paused: bool,
170
171    /// (OPENHCL_FRAMEBUFFER_GPA_BASE=\<number\>)
172    /// Base GPA of the fixed framebuffer mapping for underhill to read.
173    /// If a value is provided, a graphics device is exposed.
174    // TODO: send this value as an IGVM device tree parameter instead
175    pub framebuffer_gpa_base: Option<u64>,
176
177    /// (OPENHCL_SERIAL_WAIT_FOR_RTS=\<bool\>)
178    /// Whether the emulated 16550 waits for guest DTR+RTS before pulling data
179    /// from the host.
180    pub serial_wait_for_rts: bool,
181
182    /// (OPENHCL_FORCE_LOAD_VTL0_IMAGE=\<string\>)
183    /// Force load the specified image in VTL0. The image must support the
184    /// option specified.
185    ///
186    /// Valid options are "pcat, uefi, linux".
187    pub force_load_vtl0_image: Option<String>,
188
189    /// (OPENHCL_NVME_VFIO=1)
190    /// Use the user-mode VFIO NVMe driver instead of the Linux driver.
191    pub nvme_vfio: bool,
192
193    /// (OPENHCL_MCR_DEVICE=1)
194    /// MCR Device Enable
195    pub mcr: bool, // TODO MCR: support closed-source ENV vars
196
197    /// (OPENHCL_HIDE_ISOLATION=1)
198    /// Hide the isolation mode from the guest.
199    pub hide_isolation: bool,
200
201    /// (OPENHCL_HALT_ON_GUEST_HALT=1) When receiving a halt request from a
202    /// lower VTL, halt underhill instead of forwarding the halt request to the
203    /// host. This allows for debugging state without the partition state
204    /// changing from the host.
205    pub halt_on_guest_halt: bool,
206
207    /// (OPENHCL_NO_SIDECAR_HOTPLUG=1) Leave sidecar VPs remote even if they
208    /// hit exits.
209    pub no_sidecar_hotplug: bool,
210
211    /// (OPENHCL_NVME_KEEP_ALIVE=\<KeepaliveConfig\>)
212    /// Configure NVMe keep alive behavior when servicing.
213    /// Options are:
214    ///  - "host,privatepool" - Enable keep alive if both host and private pool support it.
215    ///  - "nohost,privatepool" - Used when the host does not support keepalive, but a private pool is present. Keepalive is disabled.
216    ///  - "nohost,noprivatepool" - Keepalive is disabled.
217    ///  - "disabled, X, X" - Keepalive is disabled due to manual
218    ///    override. Host and private pool options are ignored.
219    pub nvme_keep_alive: KeepAliveConfig,
220
221    /// (OPENHCL_MANA_KEEP_ALIVE=\<KeepAliveConfig\>)
222    /// Configure MANA keep alive behavior when servicing.
223    /// Options are:
224    ///  - "host,privatepool" - Enable keep alive if both host and private pool support it.
225    ///  - "nohost,privatepool" - Used when the host does not support keepalive, but a private pool is present. Keepalive is disabled.
226    ///  - "nohost,noprivatepool" - Keepalive is disabled.
227    ///  - "disabled, X, X" - TODO: This needs to be implemented for mana.
228    pub mana_keep_alive: KeepAliveConfig,
229
230    /// (OPENHCL_NVME_ALWAYS_FLR=1)
231    /// Always use the FLR (Function Level Reset) path for NVMe devices,
232    /// even if we would otherwise attempt to use VFIO's NoReset support.
233    pub nvme_always_flr: bool,
234
235    /// (OPENHCL_TEST_CONFIG=\<TestScenarioConfig\>)
236    /// Test configurations are designed to replicate specific behaviors and
237    /// conditions in order to simulate various test scenarios.
238    pub test_configuration: Option<TestScenarioConfig>,
239
240    /// (OPENHCL_DISABLE_UEFI_FRONTPAGE=1) Disable the frontpage in UEFI which
241    /// will result in UEFI terminating, shutting down the guest instead of
242    /// showing the frontpage.
243    pub disable_uefi_frontpage: Option<bool>,
244
245    /// (HCL_DEFAULT_BOOT_ALWAYS_ATTEMPT=1) Instruct UEFI to always attempt a
246    /// default boot, even if existing boot entries fail.
247    pub default_boot_always_attempt: Option<bool>,
248
249    /// (HCL_GUEST_STATE_LIFETIME=\<GuestStateLifetimeCli\>)
250    /// Specify which guest state lifetime to use.
251    pub guest_state_lifetime: Option<GuestStateLifetimeCli>,
252
253    /// (HCL_GUEST_STATE_ENCRYPTION_POLICY=\<GuestStateEncryptionPolicyCli\>)
254    /// Specify which guest state encryption policy to use.
255    pub guest_state_encryption_policy: Option<GuestStateEncryptionPolicyCli>,
256
257    /// (HCL_STRICT_ENCRYPTION_POLICY=1) Strict guest state encryption policy.
258    pub strict_encryption_policy: Option<bool>,
259
260    /// (HCL_ATTEMPT_AK_CERT_CALLBACK=1) Attempt to renew the AK cert.
261    /// If not specified, use the configuration in DPSv2 ManagementVtlFeatures.
262    pub attempt_ak_cert_callback: Option<bool>,
263
264    /// (OPENHCL_ENABLE_VPCI_RELAY=1) Enable the VPCI relay.
265    pub enable_vpci_relay: Option<bool>,
266
267    /// (OPENHCL_DISABLE_PROXY_REDIRECT=1) Disable proxy interrupt redirection.
268    pub disable_proxy_redirect: bool,
269
270    /// (OPENHCL_DISABLE_LOWER_VTL_TIMER_VIRT=1) Disable lower VTL timer virtualization.
271    pub disable_lower_vtl_timer_virt: bool,
272
273    /// (OPENHCL_CONFIG_TIMEOUT_IN_SECONDS=\<number\>) (default: 5)
274    /// Timeout in seconds for VM configuration operations, both initial
275    /// configuration and subsequent modifications.
276    pub config_timeout_in_seconds: u64,
277}
278
279impl Options {
280    pub(crate) fn parse(
281        extra_args: Vec<String>,
282        extra_env: Vec<(String, Option<String>)>,
283    ) -> anyhow::Result<Self> {
284        // Pull the entire environment into a BTreeMap for manipulation through extra_env.
285        let mut env: BTreeMap<OsString, OsString> = std::env::vars_os().collect();
286        for (key, value) in extra_env {
287            match value {
288                Some(value) => env.insert(key.into(), value.into()),
289                None => env.remove::<OsStr>(key.as_ref()),
290            };
291        }
292
293        // Reads an environment variable, falling back to a legacy variable (replacing
294        // "OPENHCL_" with "UNDERHILL_") if the original is not set.
295        let read_legacy_openhcl_env = |name: &str| -> Option<&OsString> {
296            env.get::<OsStr>(name.as_ref()).or_else(|| {
297                env.get::<OsStr>(
298                    format!(
299                        "UNDERHILL_{}",
300                        name.strip_prefix("OPENHCL_").unwrap_or(name)
301                    )
302                    .as_ref(),
303                )
304            })
305        };
306
307        // Reads an environment variable strings.
308        let read_env = |name: &str| -> Option<&OsString> { env.get::<OsStr>(name.as_ref()) };
309
310        fn parse_bool_opt(value: Option<&OsString>) -> anyhow::Result<Option<bool>> {
311            value
312                .map(|v| {
313                    if v.eq_ignore_ascii_case("true") || v == "1" {
314                        Ok(true)
315                    } else if v.eq_ignore_ascii_case("false") || v == "0" {
316                        Ok(false)
317                    } else {
318                        Err(anyhow::anyhow!(
319                            "invalid boolean environment variable: {}",
320                            v.to_string_lossy()
321                        ))
322                    }
323                })
324                .transpose()
325        }
326
327        fn parse_bool(value: Option<&OsString>) -> bool {
328            parse_bool_opt(value).ok().flatten().unwrap_or_default()
329        }
330
331        let parse_legacy_env_bool = |name| parse_bool(read_legacy_openhcl_env(name));
332        let parse_env_bool = |name: &str| parse_bool(read_env(name));
333        let parse_env_bool_opt = |name: &str| {
334            parse_bool_opt(read_env(name))
335                .map_err(|e| tracing::warn!("failed to parse {name}: {e:#}"))
336                .ok()
337                .flatten()
338        };
339
340        fn parse_number(value: Option<&OsString>) -> anyhow::Result<Option<u64>> {
341            value
342                .map(|v| {
343                    let v = v.to_string_lossy();
344                    v.parse()
345                        .context(format!("invalid numeric environment variable: {v}"))
346                })
347                .transpose()
348        }
349
350        let parse_legacy_env_number = |name| {
351            parse_number(read_legacy_openhcl_env(name))
352                .context(format!("parsing legacy env number: {name}"))
353        };
354
355        let mut wait_for_start = parse_legacy_env_bool("OPENHCL_WAIT_FOR_START");
356        let mut reformat_vmgs = parse_legacy_env_bool("OPENHCL_REFORMAT_VMGS");
357        let mut pid = read_legacy_openhcl_env("OPENHCL_PID_FILE_PATH")
358            .map(|x| x.to_string_lossy().into_owned().into());
359        let vmbus_max_version = read_legacy_openhcl_env("OPENHCL_VMBUS_MAX_VERSION")
360            .map(|x| {
361                vmbus_core::parse_vmbus_version(&(x.to_string_lossy()))
362                    .map_err(|x| anyhow::anyhow!("Error parsing vmbus max version: {}", x))
363            })
364            .transpose()?;
365        let vmbus_enable_mnf =
366            read_legacy_openhcl_env("OPENHCL_VMBUS_ENABLE_MNF").map(|v| parse_bool(Some(v)));
367        let vmbus_force_confidential_external_memory =
368            parse_env_bool("OPENHCL_VMBUS_FORCE_CONFIDENTIAL_EXTERNAL_MEMORY");
369        let vmbus_channel_unstick_delay_ms =
370            parse_legacy_env_number("OPENHCL_VMBUS_CHANNEL_UNSTICK_DELAY_MS")?;
371        let cmdline_append = read_legacy_openhcl_env("OPENHCL_CMDLINE_APPEND")
372            .map(|x| x.to_string_lossy().into_owned());
373        let force_load_vtl0_image = read_legacy_openhcl_env("OPENHCL_FORCE_LOAD_VTL0_IMAGE")
374            .map(|x| x.to_string_lossy().into_owned());
375        let mut vnc_port = parse_legacy_env_number("OPENHCL_VNC_PORT")?.map(|x| x as u32);
376        let framebuffer_gpa_base = parse_legacy_env_number("OPENHCL_FRAMEBUFFER_GPA_BASE")?;
377        let vtl0_starts_paused = parse_legacy_env_bool("OPENHCL_VTL0_STARTS_PAUSED");
378        let serial_wait_for_rts = parse_legacy_env_bool("OPENHCL_SERIAL_WAIT_FOR_RTS");
379        let nvme_vfio = parse_legacy_env_bool("OPENHCL_NVME_VFIO");
380        let mcr = parse_legacy_env_bool("OPENHCL_MCR_DEVICE");
381        let hide_isolation = parse_env_bool("OPENHCL_HIDE_ISOLATION");
382        let halt_on_guest_halt = parse_legacy_env_bool("OPENHCL_HALT_ON_GUEST_HALT");
383        let no_sidecar_hotplug = parse_legacy_env_bool("OPENHCL_NO_SIDECAR_HOTPLUG");
384        let gdbstub = parse_legacy_env_bool("OPENHCL_GDBSTUB");
385        let gdbstub_port = parse_legacy_env_number("OPENHCL_GDBSTUB_PORT")?.map(|x| x as u32);
386        let nvme_keep_alive = read_env("OPENHCL_NVME_KEEP_ALIVE")
387                    .map(|x| {
388                        let s = x.to_string_lossy();
389                        match s.parse::<KeepAliveConfig>() {
390                            Ok(v) => v,
391                            Err(e) => {
392                                tracing::warn!(
393                                    "failed to parse OPENHCL_NVME_KEEP_ALIVE ('{s}'): {e}. Nvme keepalive will be disabled."
394                                );
395                                KeepAliveConfig::Disabled
396                            }
397                        }
398                    })
399                    .unwrap_or(KeepAliveConfig::Disabled);
400        let mana_keep_alive = read_env("OPENHCL_MANA_KEEP_ALIVE")
401                    .map(|x| {
402                        let s = x.to_string_lossy();
403                        match s.parse::<KeepAliveConfig>() {
404                            Ok(v) => v,
405                            Err(e) => {
406                                tracing::warn!(
407                                    "failed to parse OPENHCL_MANA_KEEP_ALIVE ('{s}'): {e}. Mana keepalive will be disabled."
408                                );
409                                KeepAliveConfig::Disabled
410                            }
411                        }
412                    })
413                    .unwrap_or(KeepAliveConfig::Disabled);
414        let nvme_always_flr = parse_env_bool("OPENHCL_NVME_ALWAYS_FLR");
415        let test_configuration = read_env("OPENHCL_TEST_CONFIG").and_then(|x| {
416            x.to_string_lossy()
417                .parse::<TestScenarioConfig>()
418                .map_err(|e| {
419                    tracing::warn!(
420                        "failed to parse OPENHCL_TEST_CONFIG: {}. No test will be simulated.",
421                        e
422                    )
423                })
424                .ok()
425        });
426        let disable_uefi_frontpage = parse_env_bool_opt("OPENHCL_DISABLE_UEFI_FRONTPAGE");
427        let signal_vtl0_started = parse_env_bool("OPENHCL_SIGNAL_VTL0_STARTED");
428        let default_boot_always_attempt = parse_env_bool_opt("HCL_DEFAULT_BOOT_ALWAYS_ATTEMPT");
429        let guest_state_lifetime = read_env("HCL_GUEST_STATE_LIFETIME").and_then(|x| {
430            x.to_string_lossy()
431                .parse::<GuestStateLifetimeCli>()
432                .map_err(|e| tracing::warn!("failed to parse HCL_GUEST_STATE_LIFETIME: {:#}", e))
433                .ok()
434        });
435        let guest_state_encryption_policy =
436            read_env("HCL_GUEST_STATE_ENCRYPTION_POLICY").and_then(|x| {
437                x.to_string_lossy()
438                    .parse::<GuestStateEncryptionPolicyCli>()
439                    .map_err(|e| {
440                        tracing::warn!("failed to parse HCL_GUEST_STATE_ENCRYPTION_POLICY: {:#}", e)
441                    })
442                    .ok()
443            });
444        let strict_encryption_policy = parse_env_bool_opt("HCL_STRICT_ENCRYPTION_POLICY");
445        let attempt_ak_cert_callback = parse_env_bool_opt("HCL_ATTEMPT_AK_CERT_CALLBACK");
446        let enable_vpci_relay = parse_env_bool_opt("OPENHCL_ENABLE_VPCI_RELAY");
447        let disable_proxy_redirect = parse_env_bool("OPENHCL_DISABLE_PROXY_REDIRECT");
448        let disable_lower_vtl_timer_virt = parse_env_bool("OPENHCL_DISABLE_LOWER_VTL_TIMER_VIRT");
449        let config_timeout_in_seconds =
450            parse_legacy_env_number("OPENHCL_CONFIG_TIMEOUT_IN_SECONDS")?.unwrap_or(5);
451
452        let mut args = std::env::args().chain(extra_args);
453        // Skip our own filename.
454        args.next();
455
456        while let Some(next) = args.next() {
457            let arg = next;
458
459            match &*arg {
460                "--wait-for-start" => wait_for_start = true,
461                "--reformat-vmgs" => reformat_vmgs = true,
462
463                x if x.starts_with("--") && x.len() > 2 => {
464                    if let Some(eq) = arg.find('=') {
465                        let (name, value) = arg.split_at(eq);
466                        // Don't forget to exclude the '=' itself.
467                        let value = &value[1..];
468                        Self::parse_value_arg(name, value, &mut pid, &mut vnc_port)?;
469                    } else {
470                        if let Some(value) = args.next() {
471                            Self::parse_value_arg(&arg, &value, &mut pid, &mut vnc_port)?;
472                        } else {
473                            bail!("Expected a value after argument {}", arg);
474                        }
475                    }
476                }
477                x => bail!("Unrecognized argument {}", x),
478            }
479        }
480
481        Ok(Self {
482            wait_for_start,
483            signal_vtl0_started,
484            reformat_vmgs,
485            pid,
486            vmbus_max_version,
487            vmbus_enable_mnf,
488            vmbus_force_confidential_external_memory,
489            vmbus_channel_unstick_delay_ms: vmbus_channel_unstick_delay_ms.unwrap_or(100),
490            cmdline_append,
491            vnc_port: vnc_port.unwrap_or(3),
492            framebuffer_gpa_base,
493            gdbstub,
494            gdbstub_port: gdbstub_port.unwrap_or(4),
495            vtl0_starts_paused,
496            serial_wait_for_rts,
497            force_load_vtl0_image,
498            nvme_vfio,
499            mcr,
500            hide_isolation,
501            halt_on_guest_halt,
502            no_sidecar_hotplug,
503            nvme_keep_alive,
504            mana_keep_alive,
505            nvme_always_flr,
506            test_configuration,
507            disable_uefi_frontpage,
508            default_boot_always_attempt,
509            guest_state_lifetime,
510            guest_state_encryption_policy,
511            strict_encryption_policy,
512            attempt_ak_cert_callback,
513            enable_vpci_relay,
514            disable_proxy_redirect,
515            disable_lower_vtl_timer_virt,
516            config_timeout_in_seconds,
517        })
518    }
519
520    fn parse_value_arg(
521        name: &str,
522        value: &str,
523        pid: &mut Option<PathBuf>,
524        vnc_port: &mut Option<u32>,
525    ) -> anyhow::Result<()> {
526        match name {
527            "--pid" => *pid = Some(value.into()),
528            "--vnc-port" => {
529                *vnc_port = Some(
530                    value
531                        .parse()
532                        .context(format!("Error parsing VNC port {}", value))?,
533                )
534            }
535            x => bail!("Unrecognized argument {}", x),
536        }
537
538        Ok(())
539    }
540}