1#![warn(missing_docs)]
7
8use anyhow::Context;
9use anyhow::bail;
10use inspect::Inspect;
11use mesh::MeshPayload;
12use std::collections::BTreeMap;
13use std::ffi::OsStr;
14use std::ffi::OsString;
15use std::path::PathBuf;
16use std::str::FromStr;
17
18#[derive(Clone, Debug, MeshPayload)]
19pub enum TestScenarioConfig {
20 SaveFail,
21 RestoreStuck,
22 SaveStuck,
23}
24
25impl FromStr for TestScenarioConfig {
26 type Err = anyhow::Error;
27
28 fn from_str(s: &str) -> Result<TestScenarioConfig, anyhow::Error> {
29 match s {
30 "SERVICING_SAVE_FAIL" => Ok(TestScenarioConfig::SaveFail),
31 "SERVICING_RESTORE_STUCK" => Ok(TestScenarioConfig::RestoreStuck),
32 "SERVICING_SAVE_STUCK" => Ok(TestScenarioConfig::SaveStuck),
33 _ => Err(anyhow::anyhow!("Invalid test config: {}", s)),
34 }
35 }
36}
37
38#[derive(Clone, Debug, MeshPayload)]
39pub enum GuestStateLifetimeCli {
40 Default,
41 ReprovisionOnFailure,
42 Reprovision,
43 Ephemeral,
44}
45
46impl FromStr for GuestStateLifetimeCli {
47 type Err = anyhow::Error;
48
49 fn from_str(s: &str) -> Result<GuestStateLifetimeCli, anyhow::Error> {
50 match s {
51 "DEFAULT" | "0" => Ok(GuestStateLifetimeCli::Default),
52 "REPROVISION_ON_FAILURE" | "1" => Ok(GuestStateLifetimeCli::ReprovisionOnFailure),
53 "REPROVISION" | "2" => Ok(GuestStateLifetimeCli::Reprovision),
54 "EPHEMERAL" | "3" => Ok(GuestStateLifetimeCli::Ephemeral),
55 _ => Err(anyhow::anyhow!("Invalid lifetime: {}", s)),
56 }
57 }
58}
59
60#[derive(Clone, Debug, MeshPayload)]
61pub enum GuestStateEncryptionPolicyCli {
62 Auto,
63 None,
64 GspById,
65 GspKey,
66}
67
68impl FromStr for GuestStateEncryptionPolicyCli {
69 type Err = anyhow::Error;
70
71 fn from_str(s: &str) -> Result<GuestStateEncryptionPolicyCli, anyhow::Error> {
72 match s {
73 "AUTO" | "0" => Ok(GuestStateEncryptionPolicyCli::Auto),
74 "NONE" | "1" => Ok(GuestStateEncryptionPolicyCli::None),
75 "GSP_BY_ID" | "2" => Ok(GuestStateEncryptionPolicyCli::GspById),
76 "GSP_KEY" | "3" => Ok(GuestStateEncryptionPolicyCli::GspKey),
77 _ => Err(anyhow::anyhow!("Invalid encryption policy: {}", s)),
78 }
79 }
80}
81
82#[derive(Clone, Debug, MeshPayload, Inspect)]
83pub enum KeepAliveConfig {
84 EnabledHostAndPrivatePoolPresent,
85 DisabledHostAndPrivatePoolPresent,
86 Disabled,
87}
88
89impl FromStr for KeepAliveConfig {
90 type Err = anyhow::Error;
91
92 fn from_str(s: &str) -> Result<KeepAliveConfig, anyhow::Error> {
93 match s.to_lowercase().as_str() {
94 "host,privatepool" => Ok(KeepAliveConfig::EnabledHostAndPrivatePoolPresent),
95 "nohost,privatepool" => Ok(KeepAliveConfig::DisabledHostAndPrivatePoolPresent),
96 "nohost,noprivatepool" => Ok(KeepAliveConfig::Disabled),
97 x if x.starts_with("disabled,") => Ok(KeepAliveConfig::Disabled),
98 _ => Err(anyhow::anyhow!("Invalid keepalive config: {}", s)),
99 }
100 }
101}
102
103impl KeepAliveConfig {
104 pub fn is_enabled(&self) -> bool {
105 matches!(self, KeepAliveConfig::EnabledHostAndPrivatePoolPresent)
106 }
107}
108
109pub struct Options {
113 pub wait_for_start: bool,
116
117 pub signal_vtl0_started: bool,
122
123 pub reformat_vmgs: bool,
126
127 pub pid: Option<PathBuf>,
130
131 pub vmbus_max_version: Option<u32>,
134
135 pub vmbus_enable_mnf: Option<bool>,
138
139 pub vmbus_force_confidential_external_memory: bool,
145
146 pub vmbus_channel_unstick_delay_ms: u64,
150
151 pub cmdline_append: Option<String>,
154
155 pub vnc_port: u32,
158
159 pub gdbstub: bool,
162
163 pub gdbstub_port: u32,
166
167 pub vtl0_starts_paused: bool,
170
171 pub framebuffer_gpa_base: Option<u64>,
176
177 pub serial_wait_for_rts: bool,
181
182 pub force_load_vtl0_image: Option<String>,
188
189 pub nvme_vfio: bool,
192
193 pub mcr: bool, pub hide_isolation: bool,
200
201 pub halt_on_guest_halt: bool,
206
207 pub no_sidecar_hotplug: bool,
210
211 pub nvme_keep_alive: KeepAliveConfig,
220
221 pub mana_keep_alive: KeepAliveConfig,
229
230 pub nvme_always_flr: bool,
234
235 pub test_configuration: Option<TestScenarioConfig>,
239
240 pub disable_uefi_frontpage: Option<bool>,
244
245 pub default_boot_always_attempt: Option<bool>,
248
249 pub guest_state_lifetime: Option<GuestStateLifetimeCli>,
252
253 pub guest_state_encryption_policy: Option<GuestStateEncryptionPolicyCli>,
256
257 pub strict_encryption_policy: Option<bool>,
259
260 pub attempt_ak_cert_callback: Option<bool>,
263
264 pub enable_vpci_relay: Option<bool>,
266
267 pub disable_proxy_redirect: bool,
269
270 pub disable_lower_vtl_timer_virt: bool,
272
273 pub config_timeout_in_seconds: u64,
277
278 pub servicing_timeout_dump_collection_in_ms: u64,
282}
283
284impl Options {
285 pub(crate) fn parse(
286 extra_args: Vec<String>,
287 extra_env: Vec<(String, Option<String>)>,
288 ) -> anyhow::Result<Self> {
289 let mut env: BTreeMap<OsString, OsString> = std::env::vars_os().collect();
291 for (key, value) in extra_env {
292 match value {
293 Some(value) => env.insert(key.into(), value.into()),
294 None => env.remove::<OsStr>(key.as_ref()),
295 };
296 }
297
298 let read_legacy_openhcl_env = |name: &str| -> Option<&OsString> {
301 env.get::<OsStr>(name.as_ref()).or_else(|| {
302 env.get::<OsStr>(
303 format!(
304 "UNDERHILL_{}",
305 name.strip_prefix("OPENHCL_").unwrap_or(name)
306 )
307 .as_ref(),
308 )
309 })
310 };
311
312 let read_env = |name: &str| -> Option<&OsString> { env.get::<OsStr>(name.as_ref()) };
314
315 fn parse_bool_opt(value: Option<&OsString>) -> anyhow::Result<Option<bool>> {
316 value
317 .map(|v| {
318 if v.eq_ignore_ascii_case("true") || v == "1" {
319 Ok(true)
320 } else if v.eq_ignore_ascii_case("false") || v == "0" {
321 Ok(false)
322 } else {
323 Err(anyhow::anyhow!(
324 "invalid boolean environment variable: {}",
325 v.to_string_lossy()
326 ))
327 }
328 })
329 .transpose()
330 }
331
332 fn parse_bool(value: Option<&OsString>) -> bool {
333 parse_bool_opt(value).ok().flatten().unwrap_or_default()
334 }
335
336 let parse_legacy_env_bool = |name| parse_bool(read_legacy_openhcl_env(name));
337 let parse_env_bool = |name: &str| parse_bool(read_env(name));
338 let parse_env_bool_opt = |name: &str| {
339 parse_bool_opt(read_env(name))
340 .map_err(|e| tracing::warn!("failed to parse {name}: {e:#}"))
341 .ok()
342 .flatten()
343 };
344
345 fn parse_number(value: Option<&OsString>) -> anyhow::Result<Option<u64>> {
346 value
347 .map(|v| {
348 let v = v.to_string_lossy();
349 v.parse()
350 .context(format!("invalid numeric environment variable: {v}"))
351 })
352 .transpose()
353 }
354
355 let parse_legacy_env_number = |name| {
356 parse_number(read_legacy_openhcl_env(name))
357 .context(format!("parsing legacy env number: {name}"))
358 };
359 let parse_env_number = |name: &str| {
360 parse_number(read_env(name)).context(format!("parsing env number: {name}"))
361 };
362
363 let mut wait_for_start = parse_legacy_env_bool("OPENHCL_WAIT_FOR_START");
364 let mut reformat_vmgs = parse_legacy_env_bool("OPENHCL_REFORMAT_VMGS");
365 let mut pid = read_legacy_openhcl_env("OPENHCL_PID_FILE_PATH")
366 .map(|x| x.to_string_lossy().into_owned().into());
367 let vmbus_max_version = read_legacy_openhcl_env("OPENHCL_VMBUS_MAX_VERSION")
368 .map(|x| {
369 vmbus_core::parse_vmbus_version(&(x.to_string_lossy()))
370 .map_err(|x| anyhow::anyhow!("Error parsing vmbus max version: {}", x))
371 })
372 .transpose()?;
373 let vmbus_enable_mnf =
374 read_legacy_openhcl_env("OPENHCL_VMBUS_ENABLE_MNF").map(|v| parse_bool(Some(v)));
375 let vmbus_force_confidential_external_memory =
376 parse_env_bool("OPENHCL_VMBUS_FORCE_CONFIDENTIAL_EXTERNAL_MEMORY");
377 let vmbus_channel_unstick_delay_ms =
378 parse_legacy_env_number("OPENHCL_VMBUS_CHANNEL_UNSTICK_DELAY_MS")?;
379 let cmdline_append = read_legacy_openhcl_env("OPENHCL_CMDLINE_APPEND")
380 .map(|x| x.to_string_lossy().into_owned());
381 let force_load_vtl0_image = read_legacy_openhcl_env("OPENHCL_FORCE_LOAD_VTL0_IMAGE")
382 .map(|x| x.to_string_lossy().into_owned());
383 let mut vnc_port = parse_legacy_env_number("OPENHCL_VNC_PORT")?.map(|x| x as u32);
384 let framebuffer_gpa_base = parse_legacy_env_number("OPENHCL_FRAMEBUFFER_GPA_BASE")?;
385 let vtl0_starts_paused = parse_legacy_env_bool("OPENHCL_VTL0_STARTS_PAUSED");
386 let serial_wait_for_rts = parse_legacy_env_bool("OPENHCL_SERIAL_WAIT_FOR_RTS");
387 let nvme_vfio = parse_legacy_env_bool("OPENHCL_NVME_VFIO");
388 let mcr = parse_legacy_env_bool("OPENHCL_MCR_DEVICE");
389 let hide_isolation = parse_env_bool("OPENHCL_HIDE_ISOLATION");
390 let halt_on_guest_halt = parse_legacy_env_bool("OPENHCL_HALT_ON_GUEST_HALT");
391 let no_sidecar_hotplug = parse_legacy_env_bool("OPENHCL_NO_SIDECAR_HOTPLUG");
392 let gdbstub = parse_legacy_env_bool("OPENHCL_GDBSTUB");
393 let gdbstub_port = parse_legacy_env_number("OPENHCL_GDBSTUB_PORT")?.map(|x| x as u32);
394 let nvme_keep_alive = read_env("OPENHCL_NVME_KEEP_ALIVE")
395 .map(|x| {
396 let s = x.to_string_lossy();
397 match s.parse::<KeepAliveConfig>() {
398 Ok(v) => v,
399 Err(e) => {
400 tracing::warn!(
401 "failed to parse OPENHCL_NVME_KEEP_ALIVE ('{s}'): {e}. Nvme keepalive will be disabled."
402 );
403 KeepAliveConfig::Disabled
404 }
405 }
406 })
407 .unwrap_or(KeepAliveConfig::Disabled);
408 let mana_keep_alive = read_env("OPENHCL_MANA_KEEP_ALIVE")
409 .map(|x| {
410 let s = x.to_string_lossy();
411 match s.parse::<KeepAliveConfig>() {
412 Ok(v) => v,
413 Err(e) => {
414 tracing::warn!(
415 "failed to parse OPENHCL_MANA_KEEP_ALIVE ('{s}'): {e}. Mana keepalive will be disabled."
416 );
417 KeepAliveConfig::Disabled
418 }
419 }
420 })
421 .unwrap_or(KeepAliveConfig::Disabled);
422 let nvme_always_flr = parse_env_bool("OPENHCL_NVME_ALWAYS_FLR");
423 let test_configuration = read_env("OPENHCL_TEST_CONFIG").and_then(|x| {
424 x.to_string_lossy()
425 .parse::<TestScenarioConfig>()
426 .map_err(|e| {
427 tracing::warn!(
428 "failed to parse OPENHCL_TEST_CONFIG: {}. No test will be simulated.",
429 e
430 )
431 })
432 .ok()
433 });
434 let disable_uefi_frontpage = parse_env_bool_opt("OPENHCL_DISABLE_UEFI_FRONTPAGE");
435 let signal_vtl0_started = parse_env_bool("OPENHCL_SIGNAL_VTL0_STARTED");
436 let default_boot_always_attempt = parse_env_bool_opt("HCL_DEFAULT_BOOT_ALWAYS_ATTEMPT");
437 let guest_state_lifetime = read_env("HCL_GUEST_STATE_LIFETIME").and_then(|x| {
438 x.to_string_lossy()
439 .parse::<GuestStateLifetimeCli>()
440 .map_err(|e| tracing::warn!("failed to parse HCL_GUEST_STATE_LIFETIME: {:#}", e))
441 .ok()
442 });
443 let guest_state_encryption_policy =
444 read_env("HCL_GUEST_STATE_ENCRYPTION_POLICY").and_then(|x| {
445 x.to_string_lossy()
446 .parse::<GuestStateEncryptionPolicyCli>()
447 .map_err(|e| {
448 tracing::warn!("failed to parse HCL_GUEST_STATE_ENCRYPTION_POLICY: {:#}", e)
449 })
450 .ok()
451 });
452 let strict_encryption_policy = parse_env_bool_opt("HCL_STRICT_ENCRYPTION_POLICY");
453 let attempt_ak_cert_callback = parse_env_bool_opt("HCL_ATTEMPT_AK_CERT_CALLBACK");
454 let enable_vpci_relay = parse_env_bool_opt("OPENHCL_ENABLE_VPCI_RELAY");
455 let disable_proxy_redirect = parse_env_bool("OPENHCL_DISABLE_PROXY_REDIRECT");
456 let disable_lower_vtl_timer_virt = parse_env_bool("OPENHCL_DISABLE_LOWER_VTL_TIMER_VIRT");
457 let config_timeout_in_seconds =
458 parse_legacy_env_number("OPENHCL_CONFIG_TIMEOUT_IN_SECONDS")?.unwrap_or(5);
459 let servicing_timeout_dump_collection_in_ms =
460 parse_env_number("OPENHCL_SERVICING_TIMEOUT_DUMP_COLLECTION_IN_MS")?.unwrap_or(500);
461
462 let mut args = std::env::args().chain(extra_args);
463 args.next();
465
466 while let Some(next) = args.next() {
467 let arg = next;
468
469 match &*arg {
470 "--wait-for-start" => wait_for_start = true,
471 "--reformat-vmgs" => reformat_vmgs = true,
472
473 x if x.starts_with("--") && x.len() > 2 => {
474 if let Some(eq) = arg.find('=') {
475 let (name, value) = arg.split_at(eq);
476 let value = &value[1..];
478 Self::parse_value_arg(name, value, &mut pid, &mut vnc_port)?;
479 } else {
480 if let Some(value) = args.next() {
481 Self::parse_value_arg(&arg, &value, &mut pid, &mut vnc_port)?;
482 } else {
483 bail!("Expected a value after argument {}", arg);
484 }
485 }
486 }
487 x => bail!("Unrecognized argument {}", x),
488 }
489 }
490
491 Ok(Self {
492 wait_for_start,
493 signal_vtl0_started,
494 reformat_vmgs,
495 pid,
496 vmbus_max_version,
497 vmbus_enable_mnf,
498 vmbus_force_confidential_external_memory,
499 vmbus_channel_unstick_delay_ms: vmbus_channel_unstick_delay_ms.unwrap_or(100),
500 cmdline_append,
501 vnc_port: vnc_port.unwrap_or(3),
502 framebuffer_gpa_base,
503 gdbstub,
504 gdbstub_port: gdbstub_port.unwrap_or(4),
505 vtl0_starts_paused,
506 serial_wait_for_rts,
507 force_load_vtl0_image,
508 nvme_vfio,
509 mcr,
510 hide_isolation,
511 halt_on_guest_halt,
512 no_sidecar_hotplug,
513 nvme_keep_alive,
514 mana_keep_alive,
515 nvme_always_flr,
516 test_configuration,
517 disable_uefi_frontpage,
518 default_boot_always_attempt,
519 guest_state_lifetime,
520 guest_state_encryption_policy,
521 strict_encryption_policy,
522 attempt_ak_cert_callback,
523 enable_vpci_relay,
524 disable_proxy_redirect,
525 disable_lower_vtl_timer_virt,
526 config_timeout_in_seconds,
527 servicing_timeout_dump_collection_in_ms,
528 })
529 }
530
531 fn parse_value_arg(
532 name: &str,
533 value: &str,
534 pid: &mut Option<PathBuf>,
535 vnc_port: &mut Option<u32>,
536 ) -> anyhow::Result<()> {
537 match name {
538 "--pid" => *pid = Some(value.into()),
539 "--vnc-port" => {
540 *vnc_port = Some(
541 value
542 .parse()
543 .context(format!("Error parsing VNC port {}", value))?,
544 )
545 }
546 x => bail!("Unrecognized argument {}", x),
547 }
548
549 Ok(())
550 }
551}