1#![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 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 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
168pub struct Options {
172 pub wait_for_start: bool,
175
176 pub signal_vtl0_started: bool,
181
182 pub reformat_vmgs: bool,
185
186 pub pid: Option<PathBuf>,
189
190 pub vmbus_max_version: Option<u32>,
193
194 pub vmbus_enable_mnf: Option<bool>,
197
198 pub vmbus_force_confidential_external_memory: bool,
204
205 pub vmbus_force_gpa_pinning: bool,
209
210 pub vmbus_channel_unstick_delay_ms: u64,
214
215 pub cmdline_append: Option<String>,
218
219 pub vnc_port: u32,
222
223 pub gdbstub: bool,
226
227 pub gdbstub_port: u32,
230
231 pub vtl0_starts_paused: bool,
234
235 pub framebuffer_gpa_base: Option<u64>,
240
241 pub serial_wait_for_rts: bool,
245
246 pub force_load_vtl0_image: Option<String>,
252
253 pub nvme_vfio: bool,
256
257 pub hide_isolation: bool,
260
261 pub halt_on_guest_halt: bool,
266
267 pub no_sidecar_hotplug: bool,
270
271 pub nvme_keep_alive: KeepAliveConfig,
281
282 pub mana_keep_alive: KeepAliveConfig,
291
292 pub nvme_always_flr: bool,
296
297 pub test_configuration: Option<TestScenarioConfig>,
301
302 pub disable_uefi_frontpage: Option<bool>,
306
307 pub default_boot_always_attempt: Option<bool>,
310
311 pub guest_state_lifetime: Option<GuestStateLifetimeCli>,
314
315 pub guest_state_encryption_policy: Option<GuestStateEncryptionPolicyCli>,
318
319 pub hardware_sealing_policy: Option<HardwareSealingPolicyCli>,
324
325 pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
329
330 pub efi_diagnostics_rate_limit: Option<u32>,
338
339 pub strict_encryption_policy: Option<bool>,
341
342 pub attempt_ak_cert_callback: Option<bool>,
345
346 pub enable_vpci_relay: Option<bool>,
348
349 pub disable_proxy_redirect: bool,
351
352 pub disable_lower_vtl_timer_virt: bool,
354
355 pub config_timeout_in_seconds: u64,
359
360 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 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 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 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 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 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}