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 mesh::MeshPayload;
11use std::collections::BTreeMap;
12use std::ffi::OsStr;
13use std::ffi::OsString;
14use std::path::PathBuf;
15
16#[derive(Clone, Debug, MeshPayload)]
17pub enum TestScenarioConfig {
18    SaveFail,
19    RestoreStuck,
20    SaveStuck,
21}
22
23impl std::str::FromStr for TestScenarioConfig {
24    type Err = anyhow::Error;
25
26    fn from_str(s: &str) -> Result<TestScenarioConfig, anyhow::Error> {
27        match s {
28            "SERVICING_SAVE_FAIL" => Ok(TestScenarioConfig::SaveFail),
29            "SERVICING_RESTORE_STUCK" => Ok(TestScenarioConfig::RestoreStuck),
30            "SERVICING_SAVE_STUCK" => Ok(TestScenarioConfig::SaveStuck),
31            _ => Err(anyhow::anyhow!("Invalid test config: {}", s)),
32        }
33    }
34}
35
36// We've made our own parser here instead of using something like clap in order
37// to save on compiled file size. We don't need all the features a crate can provide.
38/// underhill core command-line and environment variable options.
39pub struct Options {
40    /// (OPENHCL_WAIT_FOR_START=1 | --wait-for-start)
41    ///  wait for a diagnostics start request before initializing and starting the VM
42    pub wait_for_start: bool,
43
44    /// (OPENHCL_SIGNAL_VTL0_STARTED=1)
45    /// immediately signal that VTL0 has started, before doing any
46    /// initialization. This allows VM boot to proceed even if initialization
47    /// may hang (e.g., because you specified OPENHCL_WAIT_FOR_START=1).
48    pub signal_vtl0_started: bool,
49
50    /// (OPENHCL_REFORMAT_VMGS=1 | --reformat-vmgs)
51    /// reformat the VMGS file on boot. useful for running potentially destructive VMGS tests.
52    pub reformat_vmgs: bool,
53
54    /// (OPENHCL_PID_FILE_PATH=/path/to/file | --pid /path/to/file)
55    /// write the PID to the specified path
56    pub pid: Option<PathBuf>,
57
58    /// (OPENHCL_VMBUS_MAX_VERSION=\<number\>)
59    /// limit the maximum protocol version allowed by vmbus; used for testing purposes
60    pub vmbus_max_version: Option<u32>,
61
62    /// (OPENHCL_VMBUS_ENABLE_MNF=1)
63    /// Enable handling of MNF in the Underhill vmbus server, instead of the host.
64    pub vmbus_enable_mnf: Option<bool>,
65
66    /// (OPENHCL_VMBUS_FORCE_CONFIDENTIAL_EXTERNAL_MEMORY=1)
67    /// Force the use of confidential external memory for all non-relay vmbus channels. For testing
68    /// purposes only.
69    ///
70    /// N.B.: Not all vmbus devices support this feature, so enabling it may cause failures.
71    pub vmbus_force_confidential_external_memory: bool,
72
73    /// (OPENHCL_CMDLINE_APPEND=\<string\>)
74    /// Command line to append to VTL0, only used with direct boot.
75    pub cmdline_append: Option<String>,
76
77    /// (OPENHCL_VNC_PORT=\<number\> | --vnc-port \<number\>) (default: 3)
78    /// VNC (vsock) port number
79    pub vnc_port: u32,
80
81    /// (OPENHCL_GDBSTUB=1)
82    /// Enables the GDB stub for debugging the guest.
83    pub gdbstub: bool,
84
85    /// (OPENHCL_GDBSTUB_PORT=\<number\>) (default: 4)
86    /// GDB stub (vsock) port number.
87    pub gdbstub_port: u32,
88
89    /// (OPENHCL_VTL0_STARTS_PAUSED=1)
90    /// Start with VTL0 paused
91    pub vtl0_starts_paused: bool,
92
93    /// (OPENHCL_FRAMEBUFFER_GPA_BASE=\<number\>)
94    /// Base GPA of the fixed framebuffer mapping for underhill to read.
95    /// If a value is provided, a graphics device is exposed.
96    // TODO: send this value as an IGVM device tree parameter instead
97    pub framebuffer_gpa_base: Option<u64>,
98
99    /// (OPENHCL_SERIAL_WAIT_FOR_RTS=\<bool\>)
100    /// Whether the emulated 16550 waits for guest DTR+RTS before pulling data
101    /// from the host.
102    pub serial_wait_for_rts: bool,
103
104    /// (OPENHCL_FORCE_LOAD_VTL0_IMAGE=\<string\>)
105    /// Force load the specified image in VTL0. The image must support the
106    /// option specified.
107    ///
108    /// Valid options are "pcat, uefi, linux".
109    pub force_load_vtl0_image: Option<String>,
110
111    /// (OPENHCL_NVME_VFIO=1)
112    /// Use the user-mode VFIO NVMe driver instead of the Linux driver.
113    pub nvme_vfio: bool,
114
115    /// (OPENHCL_MCR_DEVICE=1)
116    /// MCR Device Enable
117    pub mcr: bool, // TODO MCR: support closed-source ENV vars
118
119    /// (OPENHCL_ENABLE_SHARED_VISIBILITY_POOL=1)
120    /// Enable the shared visibility pool. This is enabled by default on
121    /// hardware isolated platforms, but can be enabled for testing.
122    pub enable_shared_visibility_pool: bool,
123
124    /// (OPENHCL_HIDE_ISOLATION=1)
125    /// Hide the isolation mode from the guest.
126    pub hide_isolation: bool,
127
128    /// (OPENHCL_HALT_ON_GUEST_HALT=1) When receiving a halt request from a
129    /// lower VTL, halt underhill instead of forwarding the halt request to the
130    /// host. This allows for debugging state without the partition state
131    /// changing from the host.
132    pub halt_on_guest_halt: bool,
133
134    /// (OPENHCL_NO_SIDECAR_HOTPLUG=1) Leave sidecar VPs remote even if they
135    /// hit exits.
136    pub no_sidecar_hotplug: bool,
137
138    /// (OPENHCL_NVME_KEEP_ALIVE=1) Enable nvme keep alive when servicing.
139    pub nvme_keep_alive: bool,
140
141    /// (OPENHCL_TEST_CONFIG=\<TestScenarioConfig\>)
142    /// Test configurations are designed to replicate specific behaviors and
143    /// conditions in order to simulate various test scenarios.
144    pub test_configuration: Option<TestScenarioConfig>,
145
146    /// (OPENHCL_DISABLE_UEFI_FRONTPAGE=1) Disable the frontpage in UEFI which
147    /// will result in UEFI terminating, shutting down the guest instead of
148    /// showing the frontpage.
149    pub disable_uefi_frontpage: bool,
150}
151
152impl Options {
153    pub(crate) fn parse(
154        extra_args: Vec<String>,
155        extra_env: Vec<(String, Option<String>)>,
156    ) -> anyhow::Result<Self> {
157        // Pull the entire environment into a BTreeMap for manipulation through extra_env.
158        let mut env: BTreeMap<OsString, OsString> = std::env::vars_os().collect();
159        for (key, value) in extra_env {
160            match value {
161                Some(value) => env.insert(key.into(), value.into()),
162                None => env.remove::<OsStr>(key.as_ref()),
163            };
164        }
165
166        // Reads an environment variable, falling back to a legacy variable (replacing
167        // "OPENHCL_" with "UNDERHILL_") if the original is not set.
168        let legacy_openhcl_env = |name: &str| -> Option<&OsString> {
169            env.get::<OsStr>(name.as_ref()).or_else(|| {
170                env.get::<OsStr>(
171                    format!(
172                        "UNDERHILL_{}",
173                        name.strip_prefix("OPENHCL_").unwrap_or(name)
174                    )
175                    .as_ref(),
176                )
177            })
178        };
179
180        // Reads an environment variable strings.
181        let parse_env_string =
182            |name: &str| -> Option<&OsString> { env.get::<OsStr>(name.as_ref()) };
183
184        fn parse_bool(value: Option<&OsString>) -> bool {
185            value
186                .map(|v| v.eq_ignore_ascii_case("true") || v == "1")
187                .unwrap_or_default()
188        }
189
190        let parse_legacy_env_bool = |name| parse_bool(legacy_openhcl_env(name));
191        let parse_env_bool = |name: &str| parse_bool(env.get::<OsStr>(name.as_ref()));
192
193        let parse_legacy_env_number = |name| {
194            legacy_openhcl_env(name)
195                .map(|v| {
196                    v.to_string_lossy().parse().context(format!(
197                        "Error parsing numeric environment variable {} {:?}",
198                        name, v
199                    ))
200                })
201                .transpose()
202        };
203
204        let mut wait_for_start = parse_legacy_env_bool("OPENHCL_WAIT_FOR_START");
205        let mut reformat_vmgs = parse_legacy_env_bool("OPENHCL_REFORMAT_VMGS");
206        let mut pid = legacy_openhcl_env("OPENHCL_PID_FILE_PATH")
207            .map(|x| x.to_string_lossy().into_owned().into());
208        let vmbus_max_version = legacy_openhcl_env("OPENHCL_VMBUS_MAX_VERSION")
209            .map(|x| {
210                vmbus_core::parse_vmbus_version(&(x.to_string_lossy()))
211                    .map_err(|x| anyhow::anyhow!("Error parsing vmbus max version: {}", x))
212            })
213            .transpose()?;
214        let vmbus_enable_mnf =
215            legacy_openhcl_env("OPENHCL_VMBUS_ENABLE_MNF").map(|v| parse_bool(Some(v)));
216        let vmbus_force_confidential_external_memory =
217            parse_env_bool("OPENHCL_VMBUS_FORCE_CONFIDENTIAL_EXTERNAL_MEMORY");
218        let cmdline_append =
219            legacy_openhcl_env("OPENHCL_CMDLINE_APPEND").map(|x| x.to_string_lossy().into_owned());
220        let force_load_vtl0_image = legacy_openhcl_env("OPENHCL_FORCE_LOAD_VTL0_IMAGE")
221            .map(|x| x.to_string_lossy().into_owned());
222        let mut vnc_port = parse_legacy_env_number("OPENHCL_VNC_PORT")?.map(|x| x as u32);
223        let framebuffer_gpa_base = parse_legacy_env_number("OPENHCL_FRAMEBUFFER_GPA_BASE")?;
224        let vtl0_starts_paused = parse_legacy_env_bool("OPENHCL_VTL0_STARTS_PAUSED");
225        let serial_wait_for_rts = parse_legacy_env_bool("OPENHCL_SERIAL_WAIT_FOR_RTS");
226        let nvme_vfio = parse_legacy_env_bool("OPENHCL_NVME_VFIO");
227        let mcr = parse_legacy_env_bool("OPENHCL_MCR_DEVICE");
228        let enable_shared_visibility_pool =
229            parse_legacy_env_bool("OPENHCL_ENABLE_SHARED_VISIBILITY_POOL");
230        let hide_isolation = parse_env_bool("OPENHCL_HIDE_ISOLATION");
231        let halt_on_guest_halt = parse_legacy_env_bool("OPENHCL_HALT_ON_GUEST_HALT");
232        let no_sidecar_hotplug = parse_legacy_env_bool("OPENHCL_NO_SIDECAR_HOTPLUG");
233        let gdbstub = parse_legacy_env_bool("OPENHCL_GDBSTUB");
234        let gdbstub_port = parse_legacy_env_number("OPENHCL_GDBSTUB_PORT")?.map(|x| x as u32);
235        let nvme_keep_alive = parse_env_bool("OPENHCL_NVME_KEEP_ALIVE");
236        let test_configuration = parse_env_string("OPENHCL_TEST_CONFIG").and_then(|x| {
237            x.to_string_lossy()
238                .parse::<TestScenarioConfig>()
239                .map_err(|e| {
240                    tracing::warn!(
241                        "failed to parse OPENHCL_TEST_CONFIG: {}. No test will be simulated.",
242                        e
243                    )
244                })
245                .ok()
246        });
247        let disable_uefi_frontpage = parse_env_bool("OPENHCL_DISABLE_UEFI_FRONTPAGE");
248        let signal_vtl0_started = parse_env_bool("OPENHCL_SIGNAL_VTL0_STARTED");
249
250        let mut args = std::env::args().chain(extra_args);
251        // Skip our own filename.
252        args.next();
253
254        while let Some(next) = args.next() {
255            let arg = next;
256
257            match &*arg {
258                "--wait-for-start" => wait_for_start = true,
259                "--reformat-vmgs" => reformat_vmgs = true,
260
261                x if x.starts_with("--") && x.len() > 2 => {
262                    if let Some(eq) = arg.find('=') {
263                        let (name, value) = arg.split_at(eq);
264                        // Don't forget to exclude the '=' itself.
265                        let value = &value[1..];
266                        Self::parse_value_arg(name, value, &mut pid, &mut vnc_port)?;
267                    } else {
268                        if let Some(value) = args.next() {
269                            Self::parse_value_arg(&arg, &value, &mut pid, &mut vnc_port)?;
270                        } else {
271                            bail!("Expected a value after argument {}", arg);
272                        }
273                    }
274                }
275                x => bail!("Unrecognized argument {}", x),
276            }
277        }
278
279        Ok(Self {
280            wait_for_start,
281            signal_vtl0_started,
282            reformat_vmgs,
283            pid,
284            vmbus_max_version,
285            vmbus_enable_mnf,
286            vmbus_force_confidential_external_memory,
287            cmdline_append,
288            vnc_port: vnc_port.unwrap_or(3),
289            framebuffer_gpa_base,
290            gdbstub,
291            gdbstub_port: gdbstub_port.unwrap_or(4),
292            vtl0_starts_paused,
293            serial_wait_for_rts,
294            force_load_vtl0_image,
295            nvme_vfio,
296            mcr,
297            enable_shared_visibility_pool,
298            hide_isolation,
299            halt_on_guest_halt,
300            no_sidecar_hotplug,
301            nvme_keep_alive,
302            test_configuration,
303            disable_uefi_frontpage,
304        })
305    }
306
307    fn parse_value_arg(
308        name: &str,
309        value: &str,
310        pid: &mut Option<PathBuf>,
311        vnc_port: &mut Option<u32>,
312    ) -> anyhow::Result<()> {
313        match name {
314            "--pid" => *pid = Some(value.into()),
315            "--vnc-port" => {
316                *vnc_port = Some(
317                    value
318                        .parse()
319                        .context(format!("Error parsing VNC port {}", value))?,
320                )
321            }
322            x => bail!("Unrecognized argument {}", x),
323        }
324
325        Ok(())
326    }
327}