Skip to main content

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