flowey_lib_hvlite/
install_vmm_tests_deps.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Hyper-V test pre-reqs
5
6use flowey::node::prelude::*;
7use std::collections::BTreeSet;
8
9const HYPERV_TESTS_REQUIRED_FEATURES: [&str; 3] = [
10    "Microsoft-Hyper-V",
11    "Microsoft-Hyper-V-Management-PowerShell",
12    "Microsoft-Hyper-V-Management-Clients",
13];
14
15const WHP_TESTS_REQUIRED_FEATURES: [&str; 1] = ["HypervisorPlatform"];
16
17const VIRT_REG_PATH: &str = r#"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Virtualization"#;
18
19#[derive(Serialize, Deserialize, PartialEq)]
20pub enum VmmTestsDepSelections {
21    Windows {
22        hyperv: bool,
23        whp: bool,
24        hardware_isolation: bool,
25    },
26    Linux,
27}
28
29flowey_request! {
30    pub enum Request {
31        /// Specify the necessary dependencies
32        Select(VmmTestsDepSelections),
33        /// Install the dependencies
34        Install(WriteVar<SideEffect>),
35        /// Generate a list of commands that would install the dependencies
36        GetCommands(WriteVar<Vec<String>>),
37    }
38}
39
40new_flow_node!(struct Node);
41
42impl FlowNode for Node {
43    type Request = Request;
44
45    fn imports(_ctx: &mut ImportCtx<'_>) {}
46
47    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
48        let mut selections = None;
49        let mut installed = Vec::new();
50        let mut write_commands = Vec::new();
51        for req in requests {
52            match req {
53                Request::Select(v) => same_across_all_reqs("Select", &mut selections, v)?,
54                Request::Install(v) => installed.push(v),
55                Request::GetCommands(v) => write_commands.push(v),
56            }
57        }
58        let selections = selections.ok_or(anyhow::anyhow!("Missing essential request: Select"))?;
59        let installed = installed;
60        let write_commands = write_commands;
61        if installed.is_empty() && write_commands.is_empty() {
62            return Ok(());
63        }
64        let installing = !installed.is_empty();
65
66        match selections {
67            VmmTestsDepSelections::Windows {
68                hyperv,
69                whp,
70                hardware_isolation,
71            } => {
72                ctx.emit_rust_step("install vmm tests deps (windows)", move |ctx| {
73                    installed.claim(ctx);
74                    let write_commands = write_commands.claim(ctx);
75
76                    move |rt| {
77                        let mut commands = Vec::new();
78
79                        if !matches!(rt.platform(), FlowPlatform::Windows)
80                            && !flowey_lib_common::_util::running_in_wsl(rt)
81                        {
82                            anyhow::bail!("Must be on Windows or WSL2 to install Windows deps.")
83                        }
84
85                        // TODO: add these features and reg keys to the initial CI image
86
87                        // Select required features
88                        let mut features_to_enable = BTreeSet::new();
89                        if hyperv {
90                            features_to_enable.append(&mut HYPERV_TESTS_REQUIRED_FEATURES.into());
91                        }
92                        if whp {
93                            features_to_enable.append(&mut WHP_TESTS_REQUIRED_FEATURES.into());
94                        }
95
96                        // Check if features are already enabled
97                        if installing && !features_to_enable.is_empty() {
98                            let features = flowey::shell_cmd!(rt, "DISM.exe /Online /Get-Features").output()?;
99                            assert!(features.status.success());
100                            let features = String::from_utf8_lossy(&features.stdout).to_string();
101                            let mut feature = None;
102                            for line in features.lines() {
103                                if let Some((k, v)) = line.split_once(":") {
104                                    if let Some(f) = feature {
105                                        assert_eq!(k.trim(), "State");
106                                        match v.trim() {
107                                            "Enabled" => {
108                                                assert!(features_to_enable.remove(f));
109                                            }
110                                            "Disabled" => {}
111                                            _ => anyhow::bail!("Unknown feature enablement state"),
112                                        }
113                                        feature = None;
114                                    } else if k.trim() == "Feature Name" {
115                                        let new_feature = v.trim();
116                                        feature = features_to_enable.contains(new_feature).then_some(new_feature);
117                                    }
118                                }
119                            }
120                        }
121
122                        // Prompt before enabling when running locally
123                        if installing && !features_to_enable.is_empty() && matches!(rt.backend(), FlowBackend::Local) {
124                            let mut features_to_install_string = String::new();
125                            for feature in features_to_enable.iter() {
126                                features_to_install_string.push_str(feature);
127                                features_to_install_string.push('\n');
128                            }
129
130                            log::warn!(
131                                r#"
132================================================================================
133To run the VMM tests, the following features need to be enabled:
134{features_to_install_string}
135
136You may need to restart your system for the changes to take effect.
137
138If you're OK with installing these features, please press <enter>.
139Otherwise, press `ctrl-c` to cancel the run.
140================================================================================
141"#
142                            );
143                            let _ = std::io::stdin().read_line(&mut String::new());
144                        }
145
146                        // Install the features
147                        for feature in features_to_enable {
148                            if installing {
149                                flowey::shell_cmd!(rt, "DISM.exe /Online /NoRestart /Enable-Feature /All /FeatureName:{feature}").run()?;
150                            }
151                            commands.push(format!("DISM.exe /Online /NoRestart /Enable-Feature /All /FeatureName:{feature}"));
152                        }
153
154                        // Select required reg keys
155                        let mut reg_keys_to_set = BTreeSet::new();
156                        if hyperv {
157                            // Allow loading IGVM from file (to run custom OpenHCL firmware)
158                            reg_keys_to_set.insert("AllowFirmwareLoadFromFile");
159                            // Enable COM3 and COM4 for Hyper-V VMs so we can get the OpenHCL KMSG logs over serial
160                            reg_keys_to_set.insert("EnableAdditionalComPorts");
161
162                            if hardware_isolation {
163                                reg_keys_to_set.insert("EnableHardwareIsolation");
164                            }
165                        }
166
167                        // Check if reg keys are set
168                        if installing && !reg_keys_to_set.is_empty() {
169                            let output = flowey::shell_cmd!(rt, "reg.exe query {VIRT_REG_PATH}").output()?;
170                            if output.status.success() {
171                                let output = String::from_utf8_lossy(&output.stdout).to_string();
172                                for line in output.lines() {
173                                    let components = line.split_whitespace().collect::<Vec<_>>();
174                                    if components.len() == 3
175                                        && reg_keys_to_set.contains(components[0])
176                                        && components[1] == "REG_DWORD"
177                                        && components[2] == "0x1"
178                                    {
179                                        assert!(reg_keys_to_set.remove(components[0]));
180                                    }
181                                }
182                            }
183                        }
184
185                        // Prompt before changing registry when running locally
186                        if installing && !reg_keys_to_set.is_empty() && matches!(rt.backend(), FlowBackend::Local) {
187                            let mut reg_keys_to_set_string = String::new();
188                            for feature in reg_keys_to_set.iter() {
189                                reg_keys_to_set_string.push_str(feature);
190                                reg_keys_to_set_string.push('\n');
191                            }
192
193                            log::warn!(
194                                r#"
195================================================================================
196To run the VMM tests, the following registry keys need to be set to 1:
197{reg_keys_to_set_string}
198
199If you're OK with changing the registry, please press <enter>.
200Otherwise, press `ctrl-c` to cancel the run.
201================================================================================
202"#
203                            );
204                            let _ = std::io::stdin().read_line(&mut String::new());
205                        }
206
207                        // Modify the registry
208                        for v in reg_keys_to_set {
209                            // TODO: figure out why reg.exe is not found if I
210                            // render the command as a string first and share
211                            if installing {
212                                flowey::shell_cmd!(rt, "reg.exe add {VIRT_REG_PATH} /v {v} /t REG_DWORD /d 1 /f").run()?;
213                            }
214                            commands.push(format!("reg.exe add \"{VIRT_REG_PATH}\" /v {v} /t REG_DWORD /d 1 /f"));
215                        }
216
217                        for write_cmds in write_commands {
218                            rt.write(write_cmds, &commands);
219                        }
220
221                        Ok(())
222                    }
223                });
224            }
225            VmmTestsDepSelections::Linux => {
226                ctx.emit_rust_step("install vmm tests deps (linux)", |ctx| {
227                    installed.claim(ctx);
228                    let write_commands = write_commands.claim(ctx);
229
230                    |rt| {
231                        for write_cmds in write_commands {
232                            rt.write(write_cmds, &Vec::new());
233                        }
234
235                        Ok(())
236                    }
237                });
238            }
239        }
240
241        Ok(())
242    }
243}