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 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 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
170pub struct Options {
174 pub wait_for_start: bool,
177
178 pub signal_vtl0_started: bool,
183
184 pub reformat_vmgs: bool,
187
188 pub pid: Option<PathBuf>,
191
192 pub vmbus_max_version: Option<u32>,
195
196 pub vmbus_enable_mnf: Option<bool>,
199
200 pub vmbus_force_confidential_external_memory: bool,
206
207 pub vmbus_force_gpa_pinning: bool,
211
212 pub vmbus_channel_unstick_delay_ms: u64,
216
217 pub cmdline_append: Option<String>,
220
221 pub vnc_port: u32,
224
225 pub gdbstub: bool,
228
229 pub gdbstub_port: u32,
232
233 pub vtl0_starts_paused: bool,
236
237 pub framebuffer_gpa_base: Option<u64>,
242
243 pub serial_wait_for_rts: bool,
247
248 pub force_load_vtl0_image: Option<String>,
254
255 pub nvme_vfio: bool,
258
259 pub hide_isolation: bool,
262
263 pub halt_on_guest_halt: bool,
268
269 pub no_sidecar_hotplug: bool,
272
273 pub nvme_keep_alive: KeepAliveConfig,
283
284 pub mana_keep_alive: KeepAliveConfig,
293
294 pub nvme_always_flr: bool,
298
299 pub test_configuration: Option<TestScenarioConfig>,
303
304 pub disable_uefi_frontpage: Option<bool>,
308
309 pub default_boot_always_attempt: Option<bool>,
312
313 pub guest_state_lifetime: Option<GuestStateLifetimeCli>,
316
317 pub guest_state_encryption_policy: Option<GuestStateEncryptionPolicyCli>,
320
321 pub hardware_sealing_policy: Option<HardwareSealingPolicyCli>,
326
327 pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
331
332 pub efi_diagnostics_rate_limit: Option<u32>,
340
341 pub strict_encryption_policy: Option<bool>,
343
344 pub attempt_ak_cert_callback: Option<bool>,
347
348 pub enable_vpci_relay: Option<bool>,
350
351 pub disable_proxy_redirect: bool,
353
354 pub disable_lower_vtl_timer_virt: bool,
356
357 pub config_timeout_in_seconds: u64,
361
362 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 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 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 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 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 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}