1use flowey::node::prelude::*;
8use std::collections::BTreeMap;
9use std::collections::BTreeSet;
10use std::process::Stdio;
11
12const HYPERV_TESTS_REQUIRED_FEATURES: [&str; 3] = [
13 "Microsoft-Hyper-V",
14 "Microsoft-Hyper-V-Management-PowerShell",
15 "Microsoft-Hyper-V-Management-Clients",
16];
17
18const WHP_TESTS_REQUIRED_FEATURES: [&str; 1] = ["HypervisorPlatform"];
19
20const VIRT_REG_PATH: &str = r#"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Virtualization"#;
21const HYPERVISOR_REG_PATH: &str = r#"HKLM\System\CurrentControlSet\Control\Hypervisor"#;
22
23#[derive(Serialize, Deserialize, Debug, PartialEq)]
24pub enum VmmTestsExternalDeps {
25 Windows(VmmTestsExternalDepsWindows),
26 Linux(VmmTestsExternalDepsLinux),
27}
28
29#[derive(Serialize, Deserialize, Debug, PartialEq)]
30pub struct VmmTestsExternalDepsWindows {
31 pub hyperv: bool,
32 pub whp: bool,
33 pub hardware_isolation: bool,
34}
35
36#[derive(Serialize, Deserialize, Debug, PartialEq)]
37pub struct VmmTestsExternalDepsLinux {
38 pub hugetlb_2mb_overcommit_pages: Option<u64>,
40 pub prepare_vhost_vsock: bool,
42}
43
44flowey_config! {
45 pub struct Config {
47 pub selections: Option<VmmTestsExternalDeps>,
49 pub auto_install: Option<bool>,
55 }
56}
57
58flowey_request! {
59 pub enum Request {
60 Install(WriteVar<SideEffect>),
62 GetCommands(WriteVar<Vec<String>>),
66 }
67}
68
69new_flow_node_with_config!(struct Node);
70
71impl FlowNodeWithConfig for Node {
72 type Request = Request;
73 type Config = Config;
74
75 fn imports(_ctx: &mut ImportCtx<'_>) {}
76
77 fn emit(
78 config: Config,
79 requests: Vec<Self::Request>,
80 ctx: &mut NodeCtx<'_>,
81 ) -> anyhow::Result<()> {
82 let mut installed = Vec::new();
83 let mut write_commands = Vec::new();
84 for req in requests {
85 match req {
86 Request::Install(v) => installed.push(v),
87 Request::GetCommands(v) => write_commands.push(v),
88 }
89 }
90
91 let installed = installed;
92 let write_commands = write_commands;
93
94 if installed.is_empty() && write_commands.is_empty() {
96 return Ok(());
97 }
98
99 let selections = config
100 .selections
101 .ok_or(anyhow::anyhow!("missing config: selections"))?;
102 let auto_install = match ctx.backend() {
104 FlowBackend::Local => config
105 .auto_install
106 .ok_or_else(|| anyhow::anyhow!("Missing essential request: AutoInstall"))?,
107 FlowBackend::Ado | FlowBackend::Github => true,
109 };
110 let installing = !installed.is_empty();
111
112 match selections {
113 VmmTestsExternalDeps::Windows(selections) => {
114 ctx.emit_rust_step("install vmm tests deps (windows)", move |ctx| {
115 installed.claim(ctx);
116 let write_commands = write_commands.claim(ctx);
117
118 move |rt| {
119 install_windows_deps(
120 rt,
121 installing,
122 auto_install,
123 selections,
124 write_commands,
125 )
126 }
127 });
128 }
129 VmmTestsExternalDeps::Linux(selections) => {
130 ctx.emit_rust_step("install vmm tests deps (linux)", |ctx| {
131 installed.claim(ctx);
132 let write_commands = write_commands.claim(ctx);
133
134 move |rt| {
135 install_linux_deps(rt, installing, auto_install, selections, write_commands)
136 }
137 });
138 }
139 }
140
141 Ok(())
142 }
143}
144
145fn install_windows_deps(
146 rt: &mut RustRuntimeServices<'_>,
147 installing: bool,
148 auto_install: bool,
149 selections: VmmTestsExternalDepsWindows,
150 write_commands: Vec<WriteVar<Vec<String>, VarClaimed>>,
151) -> anyhow::Result<()> {
152 let VmmTestsExternalDepsWindows {
153 hyperv,
154 whp,
155 hardware_isolation,
156 } = selections;
157 let mut commands = Vec::new();
158 let mut needs_restart = false;
159
160 if !matches!(rt.platform(), FlowPlatform::Windows)
161 && !flowey_lib_common::_util::running_in_wsl(rt)
162 {
163 anyhow::bail!("Must be on Windows or WSL2 to install Windows deps.")
164 }
165
166 let mut features_to_enable = BTreeSet::new();
170 if hyperv {
171 features_to_enable.append(&mut HYPERV_TESTS_REQUIRED_FEATURES.into());
172 }
173 if whp {
174 features_to_enable.append(&mut WHP_TESTS_REQUIRED_FEATURES.into());
175 }
176
177 for feature in features_to_enable.iter() {
179 commands.push(format!(
180 "DISM.exe /Online /NoRestart /Enable-Feature /All /FeatureName:{feature}"
181 ));
182 }
183
184 if installing && auto_install && !features_to_enable.is_empty() {
186 let features = flowey::shell_cmd!(rt, "DISM.exe /Online /Get-Features").output()?;
187 assert!(features.status.success());
188 let features = String::from_utf8_lossy(&features.stdout).to_string();
189 let mut feature = None;
190 for line in features.lines() {
191 if let Some((k, v)) = line.split_once(":") {
192 if let Some(f) = feature {
193 assert_eq!(k.trim(), "State");
194 match v.trim() {
195 "Enabled" => {
196 assert!(features_to_enable.remove(f));
197 }
198 "Disabled" => {}
199 _ => anyhow::bail!("Unknown feature enablement state"),
200 }
201 feature = None;
202 } else if k.trim() == "Feature Name" {
203 let new_feature = v.trim();
204 feature = features_to_enable
205 .contains(new_feature)
206 .then_some(new_feature);
207 }
208 }
209 }
210 } else if installing && !auto_install && hyperv {
211 if powershell_builder::PowerShellBuilder::new()
212 .cmdlet("Get-VM")
213 .finish()
214 .build()
215 .stdin(Stdio::null())
216 .stdout(Stdio::null())
217 .stderr(Stdio::null())
218 .status()
219 .is_ok_and(|s| s.success())
220 {
221 log::info!(
222 "Verified that Hyper-V is installed, assuming related features are enabled."
223 );
224 log::info!(
225 "If you encounter issues, try re-running in an Administrator window with `--install-missing-deps`"
226 );
227 } else {
228 anyhow::bail!(
229 "Hyper-V is not installed or your user account is not in the \"Hyper-V Administrators\" group. Re-run in an Administrator window with `--install-missing-deps`"
230 );
231 }
232
233 features_to_enable.clear();
234 }
235
236 if installing
238 && auto_install
239 && !features_to_enable.is_empty()
240 && matches!(rt.backend(), FlowBackend::Local)
241 {
242 let mut features_to_install_string = String::new();
243 for feature in features_to_enable.iter() {
244 features_to_install_string.push_str(feature);
245 features_to_install_string.push('\n');
246 }
247
248 log::warn!(
249 r#"
250================================================================================
251To run the VMM tests, the following features need to be enabled:
252{features_to_install_string}
253
254You may need to restart your system for the changes to take effect.
255
256If you're OK with installing these features, please press <enter>.
257Otherwise, press `ctrl-c` to cancel the run.
258================================================================================
259"#
260 );
261 let _ = std::io::stdin().read_line(&mut String::new());
262
263 needs_restart = true;
264 }
265
266 for feature in features_to_enable {
268 if installing && auto_install {
269 flowey::shell_cmd!(
270 rt,
271 "DISM.exe /Online /NoRestart /Enable-Feature /All /FeatureName:{feature}"
272 )
273 .run()?;
274 }
275 }
276
277 let mut reg_keys_to_set = BTreeMap::new();
279 if hyperv {
280 reg_keys_to_set
282 .entry(VIRT_REG_PATH)
283 .or_insert(BTreeMap::new())
284 .insert("AllowFirmwareLoadFromFile", ("REG_DWORD", "0x1", false));
285
286 reg_keys_to_set
288 .entry(VIRT_REG_PATH)
289 .or_insert(BTreeMap::new())
290 .insert("EnableAdditionalComPorts", ("REG_DWORD", "0x1", false));
291
292 if hardware_isolation {
293 reg_keys_to_set
294 .entry(HYPERVISOR_REG_PATH)
295 .or_insert(BTreeMap::new())
296 .insert("EnableHardwareIsolation", ("REG_DWORD", "0x1", true));
297 }
298 }
299
300 for (p, k) in reg_keys_to_set.iter() {
302 for (v, (t, d, _)) in k {
303 commands.push(format!("reg.exe add \"{p}\" /v {v} /t {t} /d {d} /f"));
304 }
305 }
306
307 if installing && !reg_keys_to_set.is_empty() {
309 for (path, keys) in reg_keys_to_set.iter_mut() {
310 let output = flowey::shell_cmd!(rt, "reg.exe query {path}").output()?;
311 if output.status.success() {
312 let output = String::from_utf8_lossy(&output.stdout).to_string();
313 for line in output.lines() {
314 let components = line.split_whitespace().collect::<Vec<_>>();
315 if components.len() == 3
316 && keys
317 .get(components[0])
318 .is_some_and(|(t, d, _)| *t == components[1] && *d == components[2])
319 {
320 assert!(keys.remove(components[0]).is_some());
321 }
322 }
323 }
324 }
325 }
326
327 let reg_keys_would_require_restart = reg_keys_to_set
329 .iter()
330 .any(|(_, k)| k.iter().any(|(_, (_, _, needs_restart))| *needs_restart));
331 let reg_keys_to_set = reg_keys_to_set
332 .into_iter()
333 .flat_map(|(p, k)| k.into_iter().map(move |(v, (t, d, _))| (p, v, t, d)))
334 .collect::<Vec<_>>();
335
336 if installing && !reg_keys_to_set.is_empty() && matches!(rt.backend(), FlowBackend::Local) {
338 let mut reg_keys_to_set_string = String::new();
339 for (p, v, _, _) in reg_keys_to_set.iter() {
340 reg_keys_to_set_string.push_str(p);
341 reg_keys_to_set_string.push(' ');
342 reg_keys_to_set_string.push_str(v);
343 reg_keys_to_set_string.push('\n');
344 }
345
346 if auto_install {
347 log::warn!(
348 r#"
349================================================================================
350To run the VMM tests, the following registry keys need to be set:
351{reg_keys_to_set_string}
352
353If you're OK with changing the registry, please press <enter>.
354Otherwise, press `ctrl-c` to cancel the run.
355================================================================================
356"#
357 );
358 let _ = std::io::stdin().read_line(&mut String::new());
359
360 needs_restart |= reg_keys_would_require_restart;
361 } else {
362 anyhow::bail!(
363 r#"
364================================================================================
365To run the VMM tests, the following registry keys need to be set:
366{reg_keys_to_set_string}
367
368Please re-run in an Administrator window with `--install-missing-deps`.
369================================================================================
370"#
371 );
372 }
373 }
374
375 for (p, v, t, d) in reg_keys_to_set {
377 if installing && auto_install {
378 flowey::shell_cmd!(rt, "reg.exe add {p} /v {v} /t {t} /d {d} /f").run()?;
379 }
380 }
381
382 if needs_restart {
383 anyhow::bail!(
384 "Installed dependencies require a restart. Please restart and re-run this command"
385 );
386 }
387
388 for write_cmds in write_commands {
389 rt.write(write_cmds, &commands);
390 }
391
392 Ok(())
393}
394
395fn install_linux_deps(
396 rt: &mut RustRuntimeServices<'_>,
397 installing: bool,
398 auto_install: bool,
399 selections: VmmTestsExternalDepsLinux,
400 write_commands: Vec<WriteVar<Vec<String>, VarClaimed>>,
401) -> anyhow::Result<()> {
402 let VmmTestsExternalDepsLinux {
403 hugetlb_2mb_overcommit_pages,
404 prepare_vhost_vsock,
405 } = selections;
406
407 for write_cmds in write_commands {
409 rt.write(write_cmds, &Vec::new());
410 }
411
412 if !installing || !auto_install {
413 return Ok(());
414 }
415
416 {
418 if Path::new("/dev/kvm").exists() {
421 flowey::shell_cmd!(rt, "sudo chmod a+rw /dev/kvm").run()?;
422 }
423 if Path::new("/dev/mshv").exists() {
424 flowey::shell_cmd!(rt, "sudo chmod a+rw /dev/mshv").run()?;
425 }
426 }
427
428 if let Some(overcommit_pages) = hugetlb_2mb_overcommit_pages {
430 let hugepages_dir = Path::new("/sys/kernel/mm/hugepages/hugepages-2048kB");
431
432 let read_counter = |name: &str| -> anyhow::Result<u64> {
433 let path = hugepages_dir.join(name);
434 let value = fs_err::read_to_string(&path)?;
435 Ok(value.trim().parse()?)
436 };
437
438 let write_overcommit_script = format!(
439 "echo {overcommit_pages} | sudo tee {path}",
440 path = hugepages_dir.join("nr_overcommit_hugepages").display(),
441 );
442 flowey::shell_cmd!(rt, "sh -c {write_overcommit_script}").run()?;
443
444 let nr_hugepages = read_counter("nr_hugepages")?;
445 let free_hugepages = read_counter("free_hugepages")?;
446 let nr_overcommit_hugepages = read_counter("nr_overcommit_hugepages")?;
447 let surplus_hugepages = read_counter("surplus_hugepages")?;
448
449 log::info!("2 MiB hugetlb nr_hugepages={nr_hugepages}");
450 log::info!("2 MiB hugetlb free_hugepages={free_hugepages}");
451 log::info!("2 MiB hugetlb nr_overcommit_hugepages={nr_overcommit_hugepages}");
452 log::info!("2 MiB hugetlb surplus_hugepages={surplus_hugepages}");
453
454 if nr_overcommit_hugepages < overcommit_pages {
455 anyhow::bail!(
456 "2 MiB hugetlb overcommit remains {}, below requested {}",
457 nr_overcommit_hugepages,
458 overcommit_pages
459 );
460 }
461 }
462
463 if prepare_vhost_vsock {
465 flowey::shell_cmd!(rt, "sudo modprobe vhost_vsock").run()?;
466 flowey::shell_cmd!(rt, "sudo udevadm settle").run()?;
474 if !Path::new("/dev/vhost-vsock").exists() {
475 anyhow::bail!("/dev/vhost-vsock did not appear after loading vhost_vsock");
476 }
477 flowey::shell_cmd!(rt, "sudo chmod a+rw /dev/vhost-vsock").run()?;
478 fs_err::OpenOptions::new()
482 .read(true)
483 .write(true)
484 .open("/dev/vhost-vsock")
485 .context("/dev/vhost-vsock is not accessible to the test user")?;
486 }
487
488 Ok(())
489}