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 = config.custom_disk_policy;
84 let custom_cache_dir = config.custom_cache_dir;
85
86 let persistent_dir = ctx.persistent_dir();
87
88 let azcopy_bin = ctx.reqv(flowey_lib_common::download_azcopy::Request::GetAzCopy);
89
90 let (files_to_download, write_files_to_download) = ctx.new_var::<Vec<(String, u64)>>();
91 let (output_folder, write_output_folder) = ctx.new_var();
92
93 ctx.emit_rust_step("calculating required VMM tests disk images", |ctx| {
94 let persistent_dir = persistent_dir.clone().claim(ctx);
95 let test_artifacts = test_artifacts.into_iter().collect::<Vec<_>>();
96 let write_files_to_download = write_files_to_download.claim(ctx);
97 let write_output_folder = write_output_folder.claim(ctx);
98 move |rt| {
99 let output_folder = if let Some(dir) = custom_cache_dir {
100 dir
101 } else if let Some(dir) = persistent_dir {
102 rt.read(dir)
103 } else {
104 std::env::current_dir()?
105 };
106
107 rt.write(write_output_folder, &output_folder.absolute()?);
108
109 let mut skip_artifacts = BTreeSet::new();
114 let mut unexpected_artifacts = BTreeSet::new();
115
116 for e in fs_err::read_dir(&output_folder)? {
117 let e = e?;
118 if e.file_type()?.is_dir() {
119 continue;
120 }
121 let filename = e.file_name();
122 let Some(filename) = filename.to_str() else {
123 continue;
124 };
125
126 if let Some(vhd) = KnownTestArtifacts::from_filename(filename) {
127 let size = e.metadata()?.len();
128 let expected_size = vhd.file_size();
129 if size != expected_size {
130 log::warn!(
131 "unexpected size for {}: expected {}, found {}",
132 filename,
133 expected_size,
134 size
135 );
136 unexpected_artifacts.insert(vhd);
137 } else {
138 skip_artifacts.insert(vhd);
139 }
140 } else {
141 continue;
142 }
143 }
144
145 if !unexpected_artifacts.is_empty() {
146 if custom_disk_policy.is_none() && matches!(rt.backend(), FlowBackend::Local) {
147 log::warn!(
148 r#"
149================================================================================
150Detected inconsistencies between expected and cached VMM test images.
151
152 If you are trying to use the same disks used in CI, then this is not expected,
153 and your cached disks are corrupt / out-of-date and need to be re-downloaded.
154 Please set the `custom_disk_policy` config to `CustomDiskPolicy::Strict`.
155
156 If you manually modified or replaced disks and you would like to keep them,
157 please set the `custom_disk_policy` config to `CustomDiskPolicy::Loose`.
158================================================================================
159"#
160 );
161 }
162
163 match custom_disk_policy {
164 Some(CustomDiskPolicy::Loose) => {
165 skip_artifacts.extend(unexpected_artifacts.iter().copied());
166 unexpected_artifacts.clear();
167 }
168 Some(CustomDiskPolicy::Strict) => {
169 log::warn!("detected inconsistent disks. will re-download them");
170 }
171 None => {
172 anyhow::bail!("detected inconsistent disks in disk cache")
173 }
174 }
175 }
176
177 let files_to_download = {
178 let mut files = Vec::new();
179
180 for artifact in test_artifacts {
181 if !skip_artifacts.contains(&artifact)
182 || unexpected_artifacts.contains(&artifact)
183 {
184 files.push((artifact.filename().to_string(), artifact.file_size()));
185 }
186 }
187
188 files.sort();
190 files
191 };
192
193 if !files_to_download.is_empty() {
194 if matches!(rt.backend(), FlowBackend::Local) {
199 let output_folder = output_folder.display();
200 let disk_image_list = files_to_download
201 .iter()
202 .map(|(name, size)| format!(" - {name} ({size})"))
203 .collect::<Vec<_>>()
204 .join("\n");
205 let download_size: u64 =
206 files_to_download.iter().map(|(_, size)| size).sum();
207 let msg = format!(
208 r#"
209================================================================================
210In order to run the selected VMM tests, some (possibly large) disk images need
211to be downloaded from Azure blob storage.
212================================================================================
213- The following disk images will be downloaded:
214{disk_image_list}
215
216- Images will be downloaded to: {output_folder}
217- The total download size is: {download_size} bytes
218
219If running locally, you can re-run with `--help` for info on how to:
220- tweak the selected download folder (e.g: download images to an external HDD)
221- skip this warning prompt in the future
222
223If you're OK with starting the download, please press just <enter>.
224Otherwise, press anything else with <enter> to cancel the run.
225================================================================================
226"#
227 );
228 log::warn!("{}", msg.trim());
229
230 let is_terminal = std::io::stdin().is_terminal();
232
233 if !skip_prompt && is_terminal {
234 let result = crossterm::event::poll(std::time::Duration::from_secs(30));
236 match result {
237 Ok(true) => {
238 if let crossterm::event::Event::Key(key_event) =
239 crossterm::event::read().unwrap()
240 {
241 if key_event.code == crossterm::event::KeyCode::Enter {
242 } else {
244 anyhow::bail!("user cancelled the run");
245 }
246 } else {
247 anyhow::bail!(
248 "unexpected event while waiting for user input"
249 );
250 }
251 }
252 Ok(false) => {
253 anyhow::bail!("timed out waiting for user input");
254 }
255 Err(e) => {
256 anyhow::bail!("error while waiting for user input: {e}");
257 }
258 }
259 }
260 }
261 }
262
263 rt.write(write_files_to_download, &files_to_download);
264 Ok(())
265 }
266 });
267
268 let did_download = ctx.emit_rust_step("downloading VMM test disk images", |ctx| {
269 let azcopy_bin = azcopy_bin.claim(ctx);
270 let files_to_download = files_to_download.claim(ctx);
271 let output_folder = output_folder.clone().claim(ctx);
272 |rt| {
273 let files_to_download = rt.read(files_to_download);
274 let output_folder = rt.read(output_folder);
275 let azcopy_bin = rt.read(azcopy_bin);
276
277 if !files_to_download.is_empty() {
278 download_blobs_from_azure(
279 rt,
280 &azcopy_bin,
281 None,
282 files_to_download,
283 &output_folder,
284 )?;
285 }
286
287 Ok(())
288 }
289 });
290
291 ctx.emit_minor_rust_step("report downloaded VMM test disk images", |ctx| {
292 did_download.claim(ctx);
293 let output_folder = output_folder.claim(ctx);
294 let get_download_folder = get_download_folder.claim(ctx);
295 |rt| {
296 let output_folder = rt.read(output_folder);
297 for path in get_download_folder {
298 rt.write(path, &output_folder)
299 }
300 }
301 });
302
303 Ok(())
304 }
305}
306
307#[expect(dead_code)]
308enum AzCopyAuthMethod {
309 AzureCli,
311 Device,
313}
314
315fn download_blobs_from_azure(
316 rt: &mut RustRuntimeServices<'_>,
319 azcopy_bin: &PathBuf,
320 azcopy_auth_method: Option<AzCopyAuthMethod>,
321 files_to_download: Vec<(String, u64)>,
322 output_folder: &Path,
323) -> anyhow::Result<()> {
324 let url = format!("https://{STORAGE_ACCOUNT}.blob.core.windows.net/{CONTAINER}/*");
328
329 let include_path = files_to_download
330 .into_iter()
331 .map(|(name, _)| name)
332 .collect::<Vec<_>>()
333 .join(";");
334
335 let auth_method = azcopy_auth_method.map(|x| match x {
337 AzCopyAuthMethod::AzureCli => "AZCLI",
338 AzCopyAuthMethod::Device => "DEVICE",
339 });
340
341 if let Some(auth_method) = auth_method {
342 rt.sh.set_var("AZCOPY_AUTO_LOGIN_TYPE", auth_method);
343 }
344 let current_dir = rt.sh.current_dir();
352 rt.sh
353 .set_var("AZCOPY_JOB_PLAN_LOCATION", current_dir.clone());
354 rt.sh.set_var("AZCOPY_LOG_LOCATION", current_dir.clone());
355
356 let result = flowey::shell_cmd!(
359 rt,
360 "{azcopy_bin} copy
361 {url}
362 {output_folder}
363 --include-path {include_path}
364 --overwrite true
365 --skip-version-check
366 "
367 )
368 .run();
369
370 if result.is_err() {
371 flowey::shell_cmd!(
372 rt,
373 "df -h --output=source,fstype,size,used,avail,pcent,target -x tmpfs -x devtmpfs"
374 )
375 .run()?;
376 let dir_contents = rt.sh.read_dir(current_dir)?;
377 for log in dir_contents
378 .iter()
379 .filter(|p| p.extension() == Some("log".as_ref()))
380 {
381 println!("{}:\n{}\n", log.display(), rt.sh.read_file(log)?);
382 }
383 return result.context("failed to download VMM test disk images");
384 }
385
386 Ok(())
387}