Skip to main content

flowey_lib_hvlite/
install_vmm_tests_external_deps.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Configure system-wide external dependencies for use with OpenVMM and/or
5//! Hyper-V VMM tests.
6
7use 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    /// If set, configure this 2 MiB hugetlb surplus page overcommit limit before running tests.
39    pub hugetlb_2mb_overcommit_pages: Option<u64>,
40    /// Load vhost-vsock and make `/dev/vhost-vsock` accessible before tests.
41    pub prepare_vhost_vsock: bool,
42}
43
44flowey_config! {
45    /// Config for the install_vmm_tests_external_deps node.
46    pub struct Config {
47        /// Specify the necessary dependencies
48        pub selections: Option<VmmTestsExternalDeps>,
49        /// Automatically install dependencies (requires admin privileges).
50        ///
51        /// When false, skip checks that require admin privileges.
52        ///
53        /// Must be set to true/false when running locally.
54        pub auto_install: Option<bool>,
55    }
56}
57
58flowey_request! {
59    pub enum Request {
60        /// Install the dependencies
61        Install(WriteVar<SideEffect>),
62        // TODO: rip this out since it is broken and not used anymore
63        // with the introduction of `vmm_tests_run_target`
64        /// Generate a list of commands that would install the dependencies
65        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        // Return if no requests specified
95        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        // Resolve auto_install for local backend
103        let auto_install = match ctx.backend() {
104            FlowBackend::Local => config
105                .auto_install
106                .ok_or_else(|| anyhow::anyhow!("Missing essential request: AutoInstall"))?,
107            // CI backends always auto-install
108            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    // TODO: add these features and reg keys to the initial CI image
167
168    // Select required features
169    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    // write commands for vmm_tests_run build only mode
178    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    // Check if features are already enabled (requires admin, so skip if not auto_install)
185    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    // Prompt before enabling when running locally
237    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    // Install the features
267    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    // Select required reg keys
278    let mut reg_keys_to_set = BTreeMap::new();
279    if hyperv {
280        // Allow loading IGVM from file (to run custom OpenHCL firmware)
281        reg_keys_to_set
282            .entry(VIRT_REG_PATH)
283            .or_insert(BTreeMap::new())
284            .insert("AllowFirmwareLoadFromFile", ("REG_DWORD", "0x1", false));
285
286        // Enable COM3 and COM4 for Hyper-V VMs so we can get the OpenHCL KMSG logs over serial
287        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    // write commands for vmm_tests_run build only mode
301    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    // Check if reg keys are set
308    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    // flatten the keys
328    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    // Prompt before changing registry when running locally
337    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    // Modify the registry
376    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    // command output not currently supported
408    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    // ensure hypervisor device is accessible
417    {
418        // Make whichever hypervisor device exists accessible.
419        // KVM machines have /dev/kvm, MSHV machines have /dev/mshv.
420        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    // ensure 2 MiB hugetlb pages are available
429    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    // prepare vhost-vsock
464    if prepare_vhost_vsock {
465        flowey::shell_cmd!(rt, "sudo modprobe vhost_vsock").run()?;
466        // The kernel creates /dev/vhost-vsock as part of
467        // loading the module, but udev then processes the
468        // corresponding uevent asynchronously and applies
469        // its own (root-only) permissions to the node.
470        // Without settling first, that races with - and
471        // frequently wins against - the chmod below,
472        // leaving the device inaccessible to the tests.
473        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        // Confirm the permissions actually stuck, so that a
479        // failure here is reported by this step instead of
480        // as an opaque test failure minutes later.
481        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}