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