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}
72
73impl FromStr for GuestStateEncryptionPolicyCli {
74 type Err = anyhow::Error;
75
76 fn from_str(s: &str) -> Result<GuestStateEncryptionPolicyCli, anyhow::Error> {
77 match s {
78 "AUTO" | "0" => Ok(GuestStateEncryptionPolicyCli::Auto),
79 "NONE" | "1" => Ok(GuestStateEncryptionPolicyCli::None),
80 "GSP_BY_ID" | "2" => Ok(GuestStateEncryptionPolicyCli::GspById),
81 "GSP_KEY" | "3" => Ok(GuestStateEncryptionPolicyCli::GspKey),
82 _ => Err(anyhow::anyhow!("Invalid encryption policy: {}", s)),
83 }
84 }
85}
86
87#[derive(Clone, Copy, Debug, MeshPayload)]
88pub enum EfiDiagnosticsLogLevelCli {
89 Default,
90 Info,
91 Full,
92}
93
94impl FromStr for EfiDiagnosticsLogLevelCli {
95 type Err = anyhow::Error;
96
97 fn from_str(s: &str) -> Result<EfiDiagnosticsLogLevelCli, anyhow::Error> {
98 match s {
99 "DEFAULT" | "0" => Ok(EfiDiagnosticsLogLevelCli::Default),
100 "INFO" | "1" => Ok(EfiDiagnosticsLogLevelCli::Info),
101 "FULL" | "2" => Ok(EfiDiagnosticsLogLevelCli::Full),
102 _ => Err(anyhow::anyhow!("Invalid EFI diagnostics log level: {}", s)),
103 }
104 }
105}
106
107#[derive(Clone, Debug, MeshPayload, Inspect, InspectMut)]
108pub enum KeepAliveConfig {
109 EnabledHostAndPrivatePoolPresent,
110 DisabledHostAndPrivatePoolPresent,
111 Disabled,
112}
113
114impl FromStr for KeepAliveConfig {
115 type Err = anyhow::Error;
116
117 fn from_str(s: &str) -> Result<KeepAliveConfig, anyhow::Error> {
118 match s.to_lowercase().as_str() {
119 "host,privatepool" | "enabled" => Ok(KeepAliveConfig::EnabledHostAndPrivatePoolPresent),
120 "nohost,privatepool" => Ok(KeepAliveConfig::DisabledHostAndPrivatePoolPresent),
121 "nohost,noprivatepool" => Ok(KeepAliveConfig::Disabled),
122 x if x == "disabled" || x.starts_with("disabled,") => Ok(KeepAliveConfig::Disabled),
123 _ => Err(anyhow::anyhow!("Invalid keepalive config: {}", s)),
124 }
125 }
126}
127
128impl KeepAliveConfig {
129 pub fn is_enabled(&self) -> bool {
130 matches!(self, KeepAliveConfig::EnabledHostAndPrivatePoolPresent)
131 }
132
133 pub fn as_str(&self) -> &'static str {
135 match self {
136 KeepAliveConfig::EnabledHostAndPrivatePoolPresent => "enabled",
137 KeepAliveConfig::DisabledHostAndPrivatePoolPresent => "nohost,privatepool",
138 KeepAliveConfig::Disabled => "disabled",
139 }
140 }
141}
142
143pub struct Options {
147 pub wait_for_start: bool,
150
151 pub signal_vtl0_started: bool,
156
157 pub reformat_vmgs: bool,
160
161 pub pid: Option<PathBuf>,
164
165 pub vmbus_max_version: Option<u32>,
168
169 pub vmbus_enable_mnf: Option<bool>,
172
173 pub vmbus_force_confidential_external_memory: bool,
179
180 pub vmbus_channel_unstick_delay_ms: u64,
184
185 pub cmdline_append: Option<String>,
188
189 pub vnc_port: u32,
192
193 pub gdbstub: bool,
196
197 pub gdbstub_port: u32,
200
201 pub vtl0_starts_paused: bool,
204
205 pub framebuffer_gpa_base: Option<u64>,
210
211 pub serial_wait_for_rts: bool,
215
216 pub force_load_vtl0_image: Option<String>,
222
223 pub nvme_vfio: bool,
226
227 pub hide_isolation: bool,
230
231 pub halt_on_guest_halt: bool,
236
237 pub no_sidecar_hotplug: bool,
240
241 pub nvme_keep_alive: KeepAliveConfig,
250
251 pub mana_keep_alive: KeepAliveConfig,
259
260 pub nvme_always_flr: bool,
264
265 pub test_configuration: Option<TestScenarioConfig>,
269
270 pub disable_uefi_frontpage: Option<bool>,
274
275 pub default_boot_always_attempt: Option<bool>,
278
279 pub guest_state_lifetime: Option<GuestStateLifetimeCli>,
282
283 pub guest_state_encryption_policy: Option<GuestStateEncryptionPolicyCli>,
286
287 pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
291
292 pub strict_encryption_policy: Option<bool>,
294
295 pub attempt_ak_cert_callback: Option<bool>,
298
299 pub enable_vpci_relay: Option<bool>,
301
302 pub disable_proxy_redirect: bool,
304
305 pub disable_lower_vtl_timer_virt: bool,
307
308 pub config_timeout_in_seconds: u64,
312
313 pub servicing_timeout_dump_collection_in_ms: u64,
317}
318
319impl Options {
320 pub(crate) fn parse(
321 extra_args: Vec<String>,
322 extra_env: Vec<(String, Option<String>)>,
323 ) -> anyhow::Result<Self> {
324 let mut env: BTreeMap<OsString, OsString> = std::env::vars_os().collect();
326 for (key, value) in extra_env {
327 match value {
328 Some(value) => env.insert(key.into(), value.into()),
329 None => env.remove::<OsStr>(key.as_ref()),
330 };
331 }
332
333 let read_legacy_openhcl_env = |name: &str| -> Option<&OsString> {
336 env.get::<OsStr>(name.as_ref()).or_else(|| {
337 env.get::<OsStr>(
338 format!(
339 "UNDERHILL_{}",
340 name.strip_prefix("OPENHCL_").unwrap_or(name)
341 )
342 .as_ref(),
343 )
344 })
345 };
346
347 let read_env = |name: &str| -> Option<&OsString> { env.get::<OsStr>(name.as_ref()) };
349
350 fn parse_bool_opt(value: Option<&OsString>) -> anyhow::Result<Option<bool>> {
351 value
352 .map(|v| {
353 if v.eq_ignore_ascii_case("true") || v == "1" {
354 Ok(true)
355 } else if v.eq_ignore_ascii_case("false") || v == "0" {
356 Ok(false)
357 } else {
358 Err(anyhow::anyhow!(
359 "invalid boolean environment variable: {}",
360 v.to_string_lossy()
361 ))
362 }
363 })
364 .transpose()
365 }
366
367 fn parse_bool(value: Option<&OsString>) -> bool {
368 parse_bool_opt(value).ok().flatten().unwrap_or_default()
369 }
370
371 let parse_legacy_env_bool = |name| parse_bool(read_legacy_openhcl_env(name));
372 let parse_env_bool = |name: &str| parse_bool(read_env(name));
373 let parse_env_bool_opt = |name: &str| {
374 parse_bool_opt(read_env(name))
375 .map_err(|e| tracing::warn!("failed to parse {name}: {e:#}"))
376 .ok()
377 .flatten()
378 };
379
380 fn parse_number(value: Option<&OsString>) -> anyhow::Result<Option<u64>> {
381 value
382 .map(|v| {
383 let v = v.to_string_lossy();
384 v.parse()
385 .context(format!("invalid numeric environment variable: {v}"))
386 })
387 .transpose()
388 }
389
390 let parse_legacy_env_number = |name| {
391 parse_number(read_legacy_openhcl_env(name))
392 .context(format!("parsing legacy env number: {name}"))
393 };
394 let parse_env_number = |name: &str| {
395 parse_number(read_env(name)).context(format!("parsing env number: {name}"))
396 };
397
398 let mut wait_for_start = parse_legacy_env_bool("OPENHCL_WAIT_FOR_START");
399 let mut reformat_vmgs = parse_legacy_env_bool("OPENHCL_REFORMAT_VMGS");
400 let mut pid = read_legacy_openhcl_env("OPENHCL_PID_FILE_PATH")
401 .map(|x| x.to_string_lossy().into_owned().into());
402 let vmbus_max_version = read_legacy_openhcl_env("OPENHCL_VMBUS_MAX_VERSION")
403 .map(|x| {
404 vmbus_core::parse_vmbus_version(&(x.to_string_lossy()))
405 .map_err(|x| anyhow::anyhow!("Error parsing vmbus max version: {}", x))
406 })
407 .transpose()?;
408 let vmbus_enable_mnf =
409 read_legacy_openhcl_env("OPENHCL_VMBUS_ENABLE_MNF").map(|v| parse_bool(Some(v)));
410 let vmbus_force_confidential_external_memory =
411 parse_env_bool("OPENHCL_VMBUS_FORCE_CONFIDENTIAL_EXTERNAL_MEMORY");
412 let vmbus_channel_unstick_delay_ms =
413 parse_legacy_env_number("OPENHCL_VMBUS_CHANNEL_UNSTICK_DELAY_MS")?;
414 let cmdline_append = read_legacy_openhcl_env("OPENHCL_CMDLINE_APPEND")
415 .map(|x| x.to_string_lossy().into_owned());
416 let force_load_vtl0_image = read_legacy_openhcl_env("OPENHCL_FORCE_LOAD_VTL0_IMAGE")
417 .map(|x| x.to_string_lossy().into_owned());
418 let mut vnc_port = parse_legacy_env_number("OPENHCL_VNC_PORT")?.map(|x| x as u32);
419 let framebuffer_gpa_base = parse_legacy_env_number("OPENHCL_FRAMEBUFFER_GPA_BASE")?;
420 let vtl0_starts_paused = parse_legacy_env_bool("OPENHCL_VTL0_STARTS_PAUSED");
421 let serial_wait_for_rts = parse_legacy_env_bool("OPENHCL_SERIAL_WAIT_FOR_RTS");
422 let nvme_vfio = parse_legacy_env_bool("OPENHCL_NVME_VFIO");
423 let hide_isolation = parse_env_bool("OPENHCL_HIDE_ISOLATION");
424 let halt_on_guest_halt = parse_legacy_env_bool("OPENHCL_HALT_ON_GUEST_HALT");
425 let no_sidecar_hotplug = parse_legacy_env_bool("OPENHCL_NO_SIDECAR_HOTPLUG");
426 let gdbstub = parse_legacy_env_bool("OPENHCL_GDBSTUB");
427 let gdbstub_port = parse_legacy_env_number("OPENHCL_GDBSTUB_PORT")?.map(|x| x as u32);
428 let nvme_keep_alive = read_env("OPENHCL_NVME_KEEP_ALIVE")
429 .map(|x| {
430 let s = x.to_string_lossy();
431 match s.parse::<KeepAliveConfig>() {
432 Ok(v) => v,
433 Err(e) => {
434 tracing::warn!(
435 "failed to parse OPENHCL_NVME_KEEP_ALIVE ('{s}'): {e}. Nvme keepalive will be disabled."
436 );
437 KeepAliveConfig::Disabled
438 }
439 }
440 })
441 .unwrap_or(KeepAliveConfig::Disabled);
442 let mana_keep_alive = read_env("OPENHCL_MANA_KEEP_ALIVE")
443 .map(|x| {
444 let s = x.to_string_lossy();
445 match s.parse::<KeepAliveConfig>() {
446 Ok(v) => v,
447 Err(e) => {
448 tracing::warn!(
449 "failed to parse OPENHCL_MANA_KEEP_ALIVE ('{s}'): {e}. Mana keepalive will be disabled."
450 );
451 KeepAliveConfig::Disabled
452 }
453 }
454 })
455 .unwrap_or(KeepAliveConfig::Disabled);
456 let nvme_always_flr = parse_env_bool("OPENHCL_NVME_ALWAYS_FLR");
457 let test_configuration = read_env("OPENHCL_TEST_CONFIG").and_then(|x| {
458 x.to_string_lossy()
459 .parse::<TestScenarioConfig>()
460 .map_err(|e| {
461 tracing::warn!(
462 "failed to parse OPENHCL_TEST_CONFIG: {}. No test will be simulated.",
463 e
464 )
465 })
466 .ok()
467 });
468 let disable_uefi_frontpage = parse_env_bool_opt("OPENHCL_DISABLE_UEFI_FRONTPAGE");
469 let signal_vtl0_started = parse_env_bool("OPENHCL_SIGNAL_VTL0_STARTED");
470 let default_boot_always_attempt = parse_env_bool_opt("HCL_DEFAULT_BOOT_ALWAYS_ATTEMPT");
471 let guest_state_lifetime = read_env("HCL_GUEST_STATE_LIFETIME").and_then(|x| {
472 x.to_string_lossy()
473 .parse::<GuestStateLifetimeCli>()
474 .map_err(|e| tracing::warn!("failed to parse HCL_GUEST_STATE_LIFETIME: {:#}", e))
475 .ok()
476 });
477 let guest_state_encryption_policy =
478 read_env("HCL_GUEST_STATE_ENCRYPTION_POLICY").and_then(|x| {
479 x.to_string_lossy()
480 .parse::<GuestStateEncryptionPolicyCli>()
481 .map_err(|e| {
482 tracing::warn!("failed to parse HCL_GUEST_STATE_ENCRYPTION_POLICY: {:#}", e)
483 })
484 .ok()
485 });
486 let efi_diagnostics_log_level = read_env("HCL_EFI_DIAGNOSTICS_LOG_LEVEL").and_then(|x| {
487 x.to_string_lossy()
488 .parse::<EfiDiagnosticsLogLevelCli>()
489 .map_err(|e| {
490 tracing::warn!("failed to parse HCL_EFI_DIAGNOSTICS_LOG_LEVEL: {:#}", e)
491 })
492 .ok()
493 });
494 let strict_encryption_policy = parse_env_bool_opt("HCL_STRICT_ENCRYPTION_POLICY");
495 let attempt_ak_cert_callback = parse_env_bool_opt("HCL_ATTEMPT_AK_CERT_CALLBACK");
496 let enable_vpci_relay = parse_env_bool_opt("OPENHCL_ENABLE_VPCI_RELAY");
497 let disable_proxy_redirect = parse_env_bool("OPENHCL_DISABLE_PROXY_REDIRECT");
498 let disable_lower_vtl_timer_virt = parse_env_bool("OPENHCL_DISABLE_LOWER_VTL_TIMER_VIRT");
499 let config_timeout_in_seconds =
500 parse_legacy_env_number("OPENHCL_CONFIG_TIMEOUT_IN_SECONDS")?.unwrap_or(5);
501 let servicing_timeout_dump_collection_in_ms =
502 parse_env_number("OPENHCL_SERVICING_TIMEOUT_DUMP_COLLECTION_IN_MS")?.unwrap_or(500);
503
504 let mut args = std::env::args().chain(extra_args);
505 args.next();
507
508 while let Some(next) = args.next() {
509 let arg = next;
510
511 match &*arg {
512 "--wait-for-start" => wait_for_start = true,
513 "--reformat-vmgs" => reformat_vmgs = true,
514
515 x if x.starts_with("--") && x.len() > 2 => {
516 if let Some(eq) = arg.find('=') {
517 let (name, value) = arg.split_at(eq);
518 let value = &value[1..];
520 Self::parse_value_arg(name, value, &mut pid, &mut vnc_port)?;
521 } else {
522 if let Some(value) = args.next() {
523 Self::parse_value_arg(&arg, &value, &mut pid, &mut vnc_port)?;
524 } else {
525 bail!("Expected a value after argument {}", arg);
526 }
527 }
528 }
529 x => bail!("Unrecognized argument {}", x),
530 }
531 }
532
533 Ok(Self {
534 wait_for_start,
535 signal_vtl0_started,
536 reformat_vmgs,
537 pid,
538 vmbus_max_version,
539 vmbus_enable_mnf,
540 vmbus_force_confidential_external_memory,
541 vmbus_channel_unstick_delay_ms: vmbus_channel_unstick_delay_ms.unwrap_or(100),
542 cmdline_append,
543 vnc_port: vnc_port.unwrap_or(3),
544 framebuffer_gpa_base,
545 gdbstub,
546 gdbstub_port: gdbstub_port.unwrap_or(4),
547 vtl0_starts_paused,
548 serial_wait_for_rts,
549 force_load_vtl0_image,
550 nvme_vfio,
551 hide_isolation,
552 halt_on_guest_halt,
553 no_sidecar_hotplug,
554 nvme_keep_alive,
555 mana_keep_alive,
556 nvme_always_flr,
557 test_configuration,
558 disable_uefi_frontpage,
559 default_boot_always_attempt,
560 guest_state_lifetime,
561 guest_state_encryption_policy,
562 efi_diagnostics_log_level,
563 strict_encryption_policy,
564 attempt_ak_cert_callback,
565 enable_vpci_relay,
566 disable_proxy_redirect,
567 disable_lower_vtl_timer_virt,
568 config_timeout_in_seconds,
569 servicing_timeout_dump_collection_in_ms,
570 })
571 }
572
573 fn parse_value_arg(
574 name: &str,
575 value: &str,
576 pid: &mut Option<PathBuf>,
577 vnc_port: &mut Option<u32>,
578 ) -> anyhow::Result<()> {
579 match name {
580 "--pid" => *pid = Some(value.into()),
581 "--vnc-port" => {
582 *vnc_port = Some(
583 value
584 .parse()
585 .context(format!("Error parsing VNC port {}", value))?,
586 )
587 }
588 x => bail!("Unrecognized argument {}", x),
589 }
590
591 Ok(())
592 }
593}