Skip to main content

flowey_lib_hvlite/_jobs/
local_run_incubator.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Prepare a standalone mshv Incubator using reusable build/acquisition nodes.
5
6use crate::common::CommonArch;
7use crate::common::CommonProfile;
8use crate::common::CommonTriple;
9use flowey::node::prelude::*;
10use sha2::Digest as _;
11
12flowey_request! {
13    pub struct Params {
14        pub profile: CommonProfile,
15        pub source_profile: String,
16        pub working_dir: PathBuf,
17        pub kernel: Option<PathBuf>,
18        pub initrd: Option<PathBuf>,
19        pub firmware: Option<PathBuf>,
20        pub boot_artifacts: Option<PathBuf>,
21        pub share: PathBuf,
22        pub dir: PathBuf,
23        pub kernel_cmdline: Option<String>,
24        pub hypervisor_cmdline: Option<String>,
25        pub prepare_only: bool,
26        pub incubator_args: Vec<String>,
27        pub done: WriteVar<SideEffect>,
28    }
29}
30
31new_simple_flow_node!(struct Node);
32
33impl SimpleFlowNode for Node {
34    type Request = Params;
35
36    fn imports(ctx: &mut ImportCtx<'_>) {
37        ctx.import::<crate::build_openvmm::Node>();
38        ctx.import::<crate::build_incubator::Node>();
39        ctx.import::<crate::build_pipette::Node>();
40        ctx.import::<crate::build_mshv_host_boot::Node>();
41        ctx.import::<crate::resolve_mshv_boot_artifacts::Node>();
42        ctx.import::<crate::resolve_openvmm_test_initrd::Node>();
43        ctx.import::<crate::resolve_openvmm_test_linux_kernel::Node>();
44        ctx.import::<crate::download_uefi_mu_msvm::Node>();
45    }
46
47    fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
48        let Params {
49            profile,
50            source_profile,
51            working_dir,
52            kernel,
53            initrd,
54            firmware,
55            boot_artifacts,
56            share,
57            dir,
58            kernel_cmdline,
59            hypervisor_cmdline,
60            prepare_only,
61            incubator_args,
62            done,
63        } = request;
64        ctx.config(crate::resolve_mshv_boot_artifacts::Config {
65            local_path: boot_artifacts.map(|path| ConfigVar(ReadVar::from_static(path))),
66        });
67        let openvmm = ctx.reqv(|openvmm| crate::build_openvmm::Request {
68            params: crate::build_openvmm::OpenvmmBuildParams {
69                target: CommonTriple::X86_64_LINUX_GNU,
70                profile,
71                features: Default::default(),
72            },
73            openvmm,
74        });
75        let incubator = ctx.reqv(|incubator| crate::build_incubator::Request {
76            target: CommonTriple::X86_64_LINUX_GNU,
77            profile: if profile.to_release() {
78                crate::run_cargo_build::BuildProfile::Release
79            } else {
80                crate::run_cargo_build::BuildProfile::Light
81            },
82            incubator,
83        });
84        let pipette = ctx.reqv(|pipette| crate::build_pipette::Request {
85            target: CommonTriple::X86_64_LINUX_MUSL,
86            profile,
87            pipette,
88        });
89        let host_boot =
90            ctx.reqv(|host_boot| crate::build_mshv_host_boot::Request { profile, host_boot });
91        let artifacts = ctx.reqv(|artifacts| crate::resolve_mshv_boot_artifacts::Request {
92            arch: CommonArch::X86_64,
93            artifacts,
94        });
95        let kernel = kernel.map(ReadVar::from_static).unwrap_or_else(|| {
96            ctx.reqv(|kernel| {
97                crate::resolve_openvmm_test_linux_kernel::Request::Get(
98                    crate::resolve_openvmm_test_linux_kernel::OpenvmmTestKernelFile::BzImage,
99                    CommonArch::X86_64,
100                    crate::resolve_openvmm_test_linux_kernel::LinuxTestKernelVersion::MshvTest,
101                    kernel,
102                )
103            })
104        });
105        let initrd = initrd.map(ReadVar::from_static).unwrap_or_else(|| {
106            ctx.reqv(|initrd| {
107                crate::resolve_openvmm_test_initrd::Request::Get(CommonArch::X86_64, initrd)
108            })
109        });
110        let firmware = firmware.map(ReadVar::from_static).unwrap_or_else(|| {
111            ctx.reqv(|msvm_fd| crate::download_uefi_mu_msvm::Request::GetMsvmFd {
112                arch: CommonArch::X86_64,
113                flavor: None,
114                msvm_fd,
115            })
116        });
117
118        ctx.emit_rust_step("prepare standalone Incubator profile and command", |ctx| {
119            let openvmm = openvmm.claim(ctx);
120            let incubator = incubator.claim(ctx);
121            let pipette = pipette.claim(ctx);
122            let host_boot = host_boot.claim(ctx);
123            let artifacts = artifacts.claim(ctx);
124            let kernel = kernel.claim(ctx);
125            let initrd = initrd.claim(ctx);
126            let firmware = firmware.claim(ctx);
127            done.claim(ctx);
128            move |rt| {
129                let runner = rt.read(incubator).bin;
130                let crate::build_openvmm::OpenvmmOutput::LinuxBin { bin: openvmm, .. } =
131                    rt.read(openvmm)
132                else {
133                    unreachable!()
134                };
135                let crate::build_pipette::PipetteOutput::LinuxBin { bin: pipette, .. } =
136                    rt.read(pipette)
137                else {
138                    unreachable!()
139                };
140                let host_boot = rt.read(host_boot).efi;
141                let artifacts = rt.read(artifacts);
142                let kernel = rt.read(kernel);
143                let firmware = rt.read(firmware);
144                let initrd = rt.read(initrd);
145                fs_err::create_dir_all(&dir)?;
146                let runtime = stage_runtime(
147                    &dir.join("runtime"),
148                    &[
149                        ("openvmm", &openvmm),
150                        ("incubator", &runner),
151                        ("pipette", &pipette),
152                        ("mshv_host_boot.efi", &host_boot),
153                    ],
154                )?;
155                let openvmm = runtime.join("openvmm");
156                let runner = runtime.join("incubator");
157                let host_boot = runtime.join("mshv_host_boot.efi");
158                let pipette_path = runtime.join("pipette");
159                let relative_pipette = pipette_path.strip_prefix(&share)?;
160                let guest_pipette = format!("/share/{}", path_str(relative_pipette)?);
161
162                let config = resolve_profile(
163                    &source_profile,
164                    &openvmm,
165                    &host_boot,
166                    &firmware,
167                    &artifacts,
168                    kernel_cmdline.as_deref(),
169                    hypervisor_cmdline.as_deref(),
170                )?;
171                let profile_path = dir.join("profile.toml");
172                let profile_file = tempfile::NamedTempFile::new_in(&dir)?;
173                fs_err::write(profile_file.path(), config)?;
174                profile_file.persist(&profile_path)?;
175                let args = vec![
176                    "--profile".into(),
177                    path_str(&profile_path)?.into(),
178                    "--kernel".into(),
179                    path_str(&kernel)?.into(),
180                    "--initrd".into(),
181                    path_str(&initrd)?.into(),
182                    "--share".into(),
183                    path_str(&share)?.into(),
184                    "--guest-pipette".into(),
185                    guest_pipette,
186                    "--output-dir".into(),
187                    path_str(&dir.join("results"))?.into(),
188                ];
189                log::info!("Prepared profile: {}", profile_path.display());
190                if prepare_only {
191                    let command = std::iter::once(path_str(&runner)?)
192                        .chain(args.iter().map(String::as_str))
193                        .chain(["--", "/bin/sh"])
194                        .map(shell_word)
195                        .collect::<Vec<_>>()
196                        .join(" ");
197                    log::info!("Run directly: {command}");
198                } else {
199                    rt.sh.change_dir(&working_dir);
200                    flowey::shell_cmd!(rt, "{runner}")
201                        .args(args)
202                        .args(incubator_args)
203                        .run_interactive()?;
204                }
205                Ok(())
206            }
207        });
208        Ok(())
209    }
210}
211
212/// Validate the provisioning recipe selected by an Incubator profile.
213pub fn parse_source_profile(contents: &str) -> anyhow::Result<toml_edit::DocumentMut> {
214    let config: toml_edit::DocumentMut = contents.parse()?;
215    let incubator = config.get("incubator").context("missing [incubator]")?;
216    let boot = config.get("boot").context("missing [boot]")?;
217    anyhow::ensure!(
218        incubator.get("type").and_then(|v| v.as_str()) == Some("openvmm")
219            && incubator.get("arch").and_then(|v| v.as_str()) == Some("x86-64")
220            && boot.get("type").and_then(|v| v.as_str()) == Some("mshv"),
221        "Incubator provisioning currently supports only type=openvmm, arch=x86-64, boot.type=mshv profiles"
222    );
223    Ok(config)
224}
225
226fn resolve_profile(
227    source: &str,
228    openvmm: &Path,
229    host_boot: &Path,
230    firmware: &Path,
231    artifacts: &Path,
232    kernel_cmdline: Option<&str>,
233    hypervisor_cmdline: Option<&str>,
234) -> anyhow::Result<String> {
235    let mut config = parse_source_profile(source)?;
236    config["incubator"]["binary"] = toml_edit::value(path_str(openvmm)?);
237    config["boot"]["host-boot"] = toml_edit::value(path_str(host_boot)?);
238    config["boot"]["firmware"] = toml_edit::value(path_str(firmware)?);
239    config["boot"]["boot-artifacts"] = toml_edit::value(path_str(artifacts)?);
240    if let Some(value) = kernel_cmdline {
241        config["boot"]["kernel-cmdline"] = toml_edit::value(value);
242    }
243    if let Some(value) = hypervisor_cmdline {
244        config["boot"]["hypervisor-cmdline"] = toml_edit::value(value);
245    }
246    Ok(config.to_string())
247}
248
249fn path_str(path: &Path) -> anyhow::Result<&str> {
250    path.to_str()
251        .with_context(|| format!("path is not valid UTF-8: {}", path.display()))
252}
253
254fn shell_word(word: &str) -> String {
255    format!("'{}'", word.replace('\'', "'\\''"))
256}
257
258// Build outputs live in Flowey's replaceable work directory. Keep an immutable
259// pair for direct runs, without copying unchanged binaries on each kernel edit.
260fn stage_runtime(root: &Path, files: &[(&str, &Path)]) -> anyhow::Result<PathBuf> {
261    let mut hash = sha2::Sha256::new();
262    let mut hashes = Vec::new();
263    for (name, source) in files {
264        let digest = sha2::Sha256::digest(fs_err::read(source)?);
265        hash.update(name.as_bytes());
266        hash.update([0]);
267        hash.update(digest);
268        hashes.push((*name, digest));
269    }
270    let key = hash
271        .finalize()
272        .iter()
273        .map(|byte| format!("{byte:02x}"))
274        .collect::<String>();
275    fs_err::create_dir_all(root)?;
276    let output = root.join(key);
277    if !output.exists() {
278        let scratch = tempfile::Builder::new()
279            .prefix(".prepare-")
280            .tempdir_in(root)?;
281        for (name, source) in files {
282            fs_err::copy(source, scratch.path().join(name))?;
283        }
284        match fs_err::rename(scratch.path(), &output) {
285            Ok(()) => {}
286            Err(error)
287                if matches!(
288                    error.kind(),
289                    std::io::ErrorKind::AlreadyExists | std::io::ErrorKind::DirectoryNotEmpty
290                ) =>
291            {
292                log::debug!("another writer staged runtime {}", output.display());
293            }
294            Err(error) => return Err(error.into()),
295        }
296    }
297    for (name, digest) in hashes {
298        let path = output.join(name);
299        anyhow::ensure!(
300            fs_err::symlink_metadata(&path)?.file_type().is_file()
301                && sha2::Sha256::digest(fs_err::read(&path)?) == digest,
302            "staged Incubator runtime was modified: {}",
303            path.display()
304        );
305    }
306    Ok(output)
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use test_with_tracing::test;
313
314    const SOURCE_PROFILE: &str =
315        include_str!("../../../../petri/incubator/profiles/x86_64-mshv.toml");
316
317    #[test]
318    fn unsupported_profiles_are_rejected_before_provisioning() {
319        for source in [
320            SOURCE_PROFILE.replace("type = \"openvmm\"", "type = \"qemu-tcg\""),
321            SOURCE_PROFILE.replace("arch = \"x86-64\"", "arch = \"aarch64\""),
322            SOURCE_PROFILE.replace("type = \"mshv\"", "type = \"linux-direct\""),
323            "[incubator]\ntype = 'openvmm'\narch = 'x86-64'\n".into(),
324        ] {
325            assert!(parse_source_profile(&source).is_err());
326        }
327    }
328
329    #[test]
330    fn source_settings_survive_path_resolution_and_explicit_overrides() -> anyhow::Result<()> {
331        let source = SOURCE_PROFILE
332            .replace("memory = \"8G\"", "memory = \"12G\"")
333            .replace("processors = 2", "processors = 4");
334        let paths = [
335            Path::new("/resolved/openvmm"),
336            Path::new("/resolved/host-boot"),
337            Path::new("/resolved/firmware"),
338            Path::new("/resolved/artifacts"),
339        ];
340        let resolved =
341            resolve_profile(&source, paths[0], paths[1], paths[2], paths[3], None, None)?;
342        let config = parse_source_profile(&resolved)?;
343        assert_eq!(config["incubator"]["memory"].as_str(), Some("12G"));
344        assert_eq!(config["incubator"]["processors"].as_integer(), Some(4));
345        assert_eq!(
346            config["incubator"]["binary"].as_str(),
347            Some("/resolved/openvmm")
348        );
349        assert_eq!(
350            config["boot"]["host-boot"].as_str(),
351            Some("/resolved/host-boot")
352        );
353        assert_eq!(
354            config["boot"]["firmware"].as_str(),
355            Some("/resolved/firmware")
356        );
357        assert_eq!(
358            config["boot"]["boot-artifacts"].as_str(),
359            Some("/resolved/artifacts")
360        );
361        let original = parse_source_profile(&source)?;
362        assert_eq!(
363            config["boot"]["kernel-cmdline"].as_str(),
364            original["boot"]["kernel-cmdline"].as_str()
365        );
366        assert_eq!(
367            config["boot"]["hypervisor-cmdline"].as_str(),
368            original["boot"]["hypervisor-cmdline"].as_str()
369        );
370        let overridden = resolve_profile(
371            &source,
372            paths[0],
373            paths[1],
374            paths[2],
375            paths[3],
376            Some("debug"),
377            Some("MSHV_ENABLE=TRUE"),
378        )?;
379        let config = parse_source_profile(&overridden)?;
380        assert_eq!(config["boot"]["kernel-cmdline"].as_str(), Some("debug"));
381        assert_eq!(
382            config["boot"]["hypervisor-cmdline"].as_str(),
383            Some("MSHV_ENABLE=TRUE")
384        );
385        Ok(())
386    }
387
388    #[test]
389    fn retains_coherent_runtime_snapshots_across_builds() -> anyhow::Result<()> {
390        let scratch = tempfile::tempdir()?;
391        let source = scratch.path().join("ephemeral-build");
392        let cache = scratch.path().join("runtime");
393        fs_err::write(&source, b"old build")?;
394        let old = stage_runtime(&cache, &[("incubator", &source)])?;
395        assert_eq!(stage_runtime(&cache, &[("incubator", &source)])?, old);
396        fs_err::write(&source, b"new build")?;
397        let new = stage_runtime(&cache, &[("incubator", &source)])?;
398        assert_ne!(new, old);
399        fs_err::remove_file(&source)?;
400        assert_eq!(fs_err::read(old.join("incubator"))?, b"old build");
401        assert_eq!(fs_err::read(new.join("incubator"))?, b"new build");
402        fs_err::write(&source, b"new build")?;
403        fs_err::write(new.join("incubator"), b"corrupt")?;
404        assert!(stage_runtime(&cache, &[("incubator", &source)]).is_err());
405        Ok(())
406    }
407}