flowey_lib_hvlite/
download_openvmm_vmm_tests_artifacts.rs1use flowey::node::prelude::*;
9use std::collections::BTreeSet;
10use std::io::IsTerminal;
11use vmm_test_images::CONTAINER;
12use vmm_test_images::KnownTestArtifacts;
13use vmm_test_images::STORAGE_ACCOUNT;
14
15#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
16pub enum CustomDiskPolicy {
17 Loose,
19 Strict,
22}
23
24flowey_config! {
25 pub struct Config {
27 pub skip_prompt: Option<bool>,
30 pub custom_disk_policy: Option<CustomDiskPolicy>,
32 pub custom_cache_dir: Option<PathBuf>,
35 }
36}
37
38flowey_request! {
39 pub enum Request {
40 Download(Vec<KnownTestArtifacts>),
42 GetDownloadFolder(WriteVar<PathBuf>),
44 }
45}
46
47new_flow_node_with_config!(struct Node);
48
49impl FlowNodeWithConfig for Node {
50 type Request = Request;
51 type Config = Config;
52
53 fn imports(ctx: &mut ImportCtx<'_>) {
54 ctx.import::<flowey_lib_common::download_azcopy::Node>();
55 ctx.import::<flowey_lib_common::install_azure_cli::Node>();
56 }
57
58 fn emit(
59 config: Config,
60 requests: Vec<Self::Request>,
61 ctx: &mut NodeCtx<'_>,
62 ) -> anyhow::Result<()> {
63 let mut test_artifacts = BTreeSet::<_>::new();
64 let mut get_download_folder = Vec::new();
65
66 for req in requests {
67 match req {
68 Request::Download(v) => v.into_iter().for_each(|v| {
69 test_artifacts.insert(v);
70 }),
71 Request::GetDownloadFolder(path) => get_download_folder.push(path),
72 }
73 }
74
75 let skip_prompt = if matches!(ctx.backend(), FlowBackend::Local) {
76 config.skip_prompt.unwrap_or(false)
77 } else {
78 if config.skip_prompt.is_some() {
79 anyhow::bail!("set `skip_prompt` config on non-local backend")
80 }
81 true
82 };
83 let custom_disk_policy = match ctx.backend() {
84 FlowBackend::Local => config.custom_disk_policy,
85 _ => Some(
87 config
88 .custom_disk_policy
89 .unwrap_or(CustomDiskPolicy::Strict),
90 ),
91 };
92 let custom_cache_dir = config.custom_cache_dir;
93
94 let persistent_dir = ctx.persistent_dir();
95
96 let azcopy_bin = ctx.reqv(flowey_lib_common::download_azcopy::Request::GetAzCopy);
97
98 let (files_to_download, write_files_to_download) = ctx.new_var::<Vec<(String, u64)>>();
99 let (output_folder, write_output_folder) = ctx.new_var();
100
101 ctx.emit_rust_step("calculating required VMM tests disk images", |ctx| {
102 let persistent_dir = persistent_dir.clone().claim(ctx);
103 let test_artifacts = test_artifacts.into_iter().collect::<Vec<_>>();
104 let write_files_to_download = write_files_to_download.claim(ctx);
105 let write_output_folder = write_output_folder.claim(ctx);
106 move |rt| {
107 let output_folder = if let Some(dir) = custom_cache_dir {
108 dir
109 } else if let Some(dir) =
110 std::env::var_os("VMM_TEST_IMAGES").and_then(|v| (!v.is_empty()).then_some(v))
111 {
112 PathBuf::from(dir)
113 } else if let Some(dir) = persistent_dir {
114 rt.read(dir)
115 } else {
116 std::env::current_dir()?
117 };
118
119 if output_folder.exists() && !output_folder.is_dir() {
120 anyhow::bail!(
121 "output dir path exists but is not a directory: {}",
122 output_folder.display()
123 );
124 }
125
126 fs_err::create_dir_all(&output_folder)?;
127
128 rt.write(write_output_folder, &output_folder.absolute()?);
129
130 let mut skip_artifacts = BTreeSet::new();
135 let mut unexpected_artifacts = BTreeSet::new();
136
137 for e in fs_err::read_dir(&output_folder)? {
138 let e = e?;
139 if e.file_type()?.is_dir() {
140 continue;
141 }
142 let filename = e.file_name();
143 let Some(filename) = filename.to_str() else {
144 continue;
145 };
146
147 if let Some(vhd) = KnownTestArtifacts::from_filename(filename) {
148 let size = e.metadata()?.len();
149 let expected_size = vhd.file_size();
150 if size != expected_size {
151 log::warn!(
152 "unexpected size for {}: expected {}, found {}",
153 filename,
154 expected_size,
155 size
156 );
157 unexpected_artifacts.insert(vhd);
158 } else {
159 skip_artifacts.insert(vhd);
160 }
161 } else {
162 continue;
163 }
164 }
165
166 if !unexpected_artifacts.is_empty() {
167 if custom_disk_policy.is_none() && matches!(rt.backend(), FlowBackend::Local) {
168 log::warn!(
169 r#"
170================================================================================
171Detected inconsistencies between expected and cached VMM test images.
172
173 If you are trying to use the same disks used in CI, then this is not expected,
174 and your cached disks are corrupt / out-of-date and need to be re-downloaded.
175 Please set the `custom_disk_policy` config to `CustomDiskPolicy::Strict`.
176
177 If you manually modified or replaced disks and you would like to keep them,
178 please set the `custom_disk_policy` config to `CustomDiskPolicy::Loose`.
179================================================================================
180"#
181 );
182 }
183
184 match custom_disk_policy {
185 Some(CustomDiskPolicy::Loose) => {
186 skip_artifacts.extend(unexpected_artifacts.iter().copied());
187 unexpected_artifacts.clear();
188 }
189 Some(CustomDiskPolicy::Strict) => {
190 log::warn!("detected inconsistent disks. will re-download them");
191 }
192 None => {
193 anyhow::bail!("detected inconsistent disks in disk cache")
194 }
195 }
196 }
197
198 let files_to_download = {
199 let mut files = Vec::new();
200
201 for artifact in test_artifacts {
202 if !skip_artifacts.contains(&artifact)
203 || unexpected_artifacts.contains(&artifact)
204 {
205 files.push((artifact.filename().to_string(), artifact.file_size()));
206 }
207 }
208
209 files.sort();
211 files
212 };
213
214 if !files_to_download.is_empty() {
215 if matches!(rt.backend(), FlowBackend::Local) {
220 let output_folder = output_folder.display();
221 let disk_image_list = files_to_download
222 .iter()
223 .map(|(name, size)| format!(" - {name} ({size})"))
224 .collect::<Vec<_>>()
225 .join("\n");
226 let download_size: u64 =
227 files_to_download.iter().map(|(_, size)| size).sum();
228 let msg = format!(
229 r#"
230================================================================================
231In order to run the selected VMM tests, some (possibly large) disk images need
232to be downloaded from Azure blob storage.
233================================================================================
234- The following disk images will be downloaded:
235{disk_image_list}
236
237- Images will be downloaded to: {output_folder}
238- The total download size is: {download_size} bytes
239
240If running locally, you can re-run with `--help` for info on how to:
241- tweak the selected download folder (e.g: download images to an external HDD)
242- skip this warning prompt in the future
243
244If you're OK with starting the download, please press just <enter>.
245Otherwise, press anything else with <enter> to cancel the run.
246================================================================================
247"#
248 );
249 log::warn!("{}", msg.trim());
250
251 let is_terminal = std::io::stdin().is_terminal();
253
254 if !skip_prompt && is_terminal {
255 let result = crossterm::event::poll(std::time::Duration::from_secs(30));
258 match result {
259 Ok(true) => {
260 if let crossterm::event::Event::Key(key_event) =
261 crossterm::event::read().unwrap()
262 {
263 if key_event.code == crossterm::event::KeyCode::Enter {
264 } else {
266 anyhow::bail!("user cancelled the run");
267 }
268 } else {
269 anyhow::bail!(
270 "unexpected event while waiting for user input"
271 );
272 }
273 }
274 Ok(false) => {
275 anyhow::bail!("timed out waiting for user input");
276 }
277 Err(e) => {
278 anyhow::bail!("error while waiting for user input: {e}");
279 }
280 }
281 }
282 }
283 }
284
285 rt.write(write_files_to_download, &files_to_download);
286 Ok(())
287 }
288 });
289
290 let did_download = ctx.emit_rust_step("downloading VMM test disk images", |ctx| {
291 let azcopy_bin = azcopy_bin.claim(ctx);
292 let files_to_download = files_to_download.claim(ctx);
293 let output_folder = output_folder.clone().claim(ctx);
294 |rt| {
295 let files_to_download = rt.read(files_to_download);
296 let output_folder = rt.read(output_folder);
297 let azcopy_bin = rt.read(azcopy_bin);
298
299 if !files_to_download.is_empty() {
300 download_blobs_from_azure(
301 rt,
302 &azcopy_bin,
303 None,
304 files_to_download,
305 &output_folder,
306 )?;
307 }
308
309 Ok(())
310 }
311 });
312
313 ctx.emit_minor_rust_step("report downloaded VMM test disk images", |ctx| {
314 did_download.claim(ctx);
315 let output_folder = output_folder.claim(ctx);
316 let get_download_folder = get_download_folder.claim(ctx);
317 |rt| {
318 let output_folder = rt.read(output_folder);
319 for path in get_download_folder {
320 rt.write(path, &output_folder)
321 }
322 }
323 });
324
325 Ok(())
326 }
327}
328
329#[expect(dead_code)]
330enum AzCopyAuthMethod {
331 AzureCli,
333 Device,
335}
336
337fn download_blobs_from_azure(
338 rt: &mut RustRuntimeServices<'_>,
341 azcopy_bin: &PathBuf,
342 azcopy_auth_method: Option<AzCopyAuthMethod>,
343 files_to_download: Vec<(String, u64)>,
344 output_folder: &Path,
345) -> anyhow::Result<()> {
346 let url = format!("https://{STORAGE_ACCOUNT}.blob.core.windows.net/{CONTAINER}/*");
350
351 let include_path = files_to_download
352 .into_iter()
353 .map(|(name, _)| name)
354 .collect::<Vec<_>>()
355 .join(";");
356
357 let auth_method = azcopy_auth_method.map(|x| match x {
359 AzCopyAuthMethod::AzureCli => "AZCLI",
360 AzCopyAuthMethod::Device => "DEVICE",
361 });
362
363 if let Some(auth_method) = auth_method {
364 rt.sh.set_var("AZCOPY_AUTO_LOGIN_TYPE", auth_method);
365 }
366 let current_dir = rt.sh.current_dir();
374 rt.sh
375 .set_var("AZCOPY_JOB_PLAN_LOCATION", current_dir.clone());
376 rt.sh.set_var("AZCOPY_LOG_LOCATION", current_dir.clone());
377
378 let result = flowey::shell_cmd!(
381 rt,
382 "{azcopy_bin} copy
383 {url}
384 {output_folder}
385 --include-path {include_path}
386 --overwrite true
387 --skip-version-check
388 "
389 )
390 .run();
391
392 if result.is_err() {
393 flowey::shell_cmd!(
394 rt,
395 "df -h --output=source,fstype,size,used,avail,pcent,target -x tmpfs -x devtmpfs"
396 )
397 .run()?;
398 let dir_contents = rt.sh.read_dir(current_dir)?;
399 for log in dir_contents
400 .iter()
401 .filter(|p| p.extension() == Some("log".as_ref()))
402 {
403 println!("{}:\n{}\n", log.display(), rt.sh.read_file(log)?);
404 }
405 return result.context("failed to download VMM test disk images");
406 }
407
408 Ok(())
409}