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