Skip to main content

flowey_lib_hvlite/
resolve_openvmm_test_linux_kernel.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Download files from a Linux test kernel `openvmm-deps` GitHub release
5//! artifact, or use a local path if specified.
6//!
7//! Each [`LinuxTestKernelVersion`] variant corresponds to its own
8//! per-kernel-version GitHub release artifact (e.g.
9//! `openvmm-test-linux-6.1.<arch>.<ver>.tar.gz`), so consumers can target
10//! different kernel versions independently. Each archive contains the
11//! primary kernel image (`vmlinux` on x86_64, `Image` on aarch64) and, on
12//! x86_64, an additional `bzImage`-format kernel — see
13//! [`OpenvmmTestKernelFile`] to select between them. The matching guest-
14//! userland initrd is shared across kernel versions and lives in its own
15//! node (see [`crate::resolve_openvmm_test_initrd`]).
16
17use crate::common::CommonArch;
18use flowey::node::prelude::*;
19use std::collections::BTreeMap;
20use std::collections::BTreeSet;
21
22/// Which Linux test kernel version to fetch from the openvmm-deps GitHub
23/// release.
24#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
25pub enum LinuxTestKernelVersion {
26    Linux6_1,
27    Linux6_18,
28    /// The `kvm-cca-dev` kernel (Linux 7.1.0-rc1 at time of writing).
29    ///
30    /// Published **aarch64-only**. Beyond the ARM CCA host bits it is built
31    /// for, it also enables the P2PDMA / vfio-dmabuf / iommufd config
32    /// (`CONFIG_PCI_P2PDMA`, `CONFIG_VFIO_PCI_DMABUF`, `CONFIG_IOMMUFD`,
33    /// `CONFIG_ARM_SMMU_V3`) that the incubator's VFIO device-assignment tests
34    /// need to exercise device-BAR P2P DMA, which predate the 6.18 test kernel.
35    KvmCcaDev,
36}
37
38impl LinuxTestKernelVersion {
39    /// The version string used in the openvmm-deps GitHub release artifact
40    /// filename (e.g. `"6.1"` for `openvmm-test-linux-6.1.<arch>.<ver>.tar.gz`).
41    pub fn artifact_tag(self) -> &'static str {
42        match self {
43            Self::Linux6_1 => "6.1",
44            Self::Linux6_18 => "6.18",
45            Self::KvmCcaDev => "kvm-cca-dev",
46        }
47    }
48
49    /// Whether this kernel version is published for the given architecture.
50    ///
51    /// Most versions ship for both architectures; `kvm-cca-dev` is aarch64-only.
52    pub fn is_available_for(self, arch: CommonArch) -> bool {
53        match self {
54            Self::Linux6_1 | Self::Linux6_18 => true,
55            Self::KvmCcaDev => matches!(arch, CommonArch::Aarch64),
56        }
57    }
58}
59
60/// Which file to extract from a per-(arch, kver) `openvmm-test-linux` archive.
61#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
62pub enum OpenvmmTestKernelFile {
63    /// Primary kernel image: `vmlinux` on x86_64, `Image` on aarch64.
64    Kernel,
65    /// `bzImage`-format kernel image. Only available on x86_64.
66    BzImage,
67}
68
69impl OpenvmmTestKernelFile {
70    /// Whether this file is shipped in the archive for the given architecture.
71    pub fn is_available_for(self, arch: CommonArch) -> bool {
72        match self {
73            Self::Kernel => true,
74            Self::BzImage => matches!(arch, CommonArch::X86_64),
75        }
76    }
77
78    /// The filename of this file inside the per-(arch, kver) archive.
79    pub fn filename(self, arch: CommonArch) -> &'static str {
80        match self {
81            Self::Kernel => match arch {
82                CommonArch::X86_64 => "vmlinux",
83                CommonArch::Aarch64 => "Image",
84            },
85            Self::BzImage => "bzImage",
86        }
87    }
88}
89
90/// The default Linux test kernel version. Call sites that don't otherwise care
91/// which kernel they're using should pass this.
92pub const DEFAULT_LINUX_TEST_KERNEL_VERSION: LinuxTestKernelVersion =
93    LinuxTestKernelVersion::Linux6_18;
94
95/// The Linux test kernel used as the **L1 host image** for the aarch64 QEMU-TCG
96/// incubator. Unlike [`DEFAULT_LINUX_TEST_KERNEL_VERSION`], this must ship the
97/// P2PDMA / vfio-dmabuf / iommufd config
98/// ([`LinuxTestKernelVersion::KvmCcaDev`], Linux 7.1.0-rc1) so incubator VFIO
99/// device-assignment tests can exercise device-BAR peer-to-peer DMA. Aarch64
100/// only (the incubator is aarch64-only).
101pub const INCUBATOR_LINUX_TEST_KERNEL_VERSION: LinuxTestKernelVersion =
102    LinuxTestKernelVersion::KvmCcaDev;
103
104flowey_config! {
105    /// Config for the resolve_openvmm_test_linux_kernel node.
106    pub struct Config {
107        /// Specify version of the github release to pull from
108        pub version: Option<String>,
109        /// Use locally downloaded openvmm-test-linux contents, keyed by
110        /// (architecture, kernel version)
111        pub local_paths: BTreeMap<(CommonArch, LinuxTestKernelVersion), ConfigVar<PathBuf>>,
112    }
113}
114
115flowey_request! {
116    pub enum Request {
117        /// Get the path to a specific file from the per-(arch, kver) archive.
118        Get(
119            OpenvmmTestKernelFile,
120            CommonArch,
121            LinuxTestKernelVersion,
122            WriteVar<PathBuf>,
123        ),
124    }
125}
126
127new_flow_node_with_config!(struct Node);
128
129impl FlowNodeWithConfig for Node {
130    type Request = Request;
131    type Config = Config;
132
133    fn imports(ctx: &mut ImportCtx<'_>) {
134        ctx.import::<flowey_lib_common::install_dist_pkg::Node>();
135        ctx.import::<flowey_lib_common::download_gh_release::Node>();
136    }
137
138    fn emit(
139        config: Config,
140        requests: Vec<Self::Request>,
141        ctx: &mut NodeCtx<'_>,
142    ) -> anyhow::Result<()> {
143        let Config {
144            version,
145            local_paths,
146        } = config;
147        let mut deps: BTreeMap<
148            (OpenvmmTestKernelFile, CommonArch, LinuxTestKernelVersion),
149            Vec<WriteVar<PathBuf>>,
150        > = BTreeMap::new();
151
152        for req in requests {
153            match req {
154                Request::Get(file, arch, kver, var) => {
155                    if !kver.is_available_for(arch) {
156                        anyhow::bail!(
157                            "test kernel {:?} is not published for {arch:?}",
158                            kver.artifact_tag()
159                        );
160                    }
161                    if !file.is_available_for(arch) {
162                        anyhow::bail!(
163                            "{file:?} is not available in the openvmm-test-linux archive for {arch:?}"
164                        );
165                    }
166                    deps.entry((file, arch, kver)).or_default().push(var);
167                }
168            }
169        }
170
171        if version.is_some() && !local_paths.is_empty() {
172            anyhow::bail!("Cannot specify both Version and LocalPath requests");
173        }
174
175        if version.is_none() && local_paths.is_empty() {
176            anyhow::bail!("Must specify a Version or LocalPath request");
177        }
178
179        // -- end of req processing -- //
180
181        if deps.is_empty() {
182            return Ok(());
183        }
184
185        if !local_paths.is_empty() {
186            ctx.emit_rust_step("use local openvmm-test-linux", |ctx| {
187                let deps = deps.claim(ctx);
188                let local_paths: BTreeMap<_, _> = local_paths
189                    .into_iter()
190                    .map(|(key, var)| (key, var.claim(ctx)))
191                    .collect();
192                move |rt| {
193                    let resolved_paths: BTreeMap<(CommonArch, LinuxTestKernelVersion), PathBuf> =
194                        local_paths
195                            .into_iter()
196                            .map(|(key, var)| (key, rt.read(var)))
197                            .collect();
198
199                    for ((file, arch, kver), vars) in deps {
200                        let base_dir = resolved_paths.get(&(arch, kver)).ok_or_else(|| {
201                            anyhow::anyhow!("No local path specified for ({:?}, {:?})", arch, kver)
202                        })?;
203                        let path = base_dir.join(file.filename(arch));
204                        rt.write_all(vars, &path)
205                    }
206
207                    Ok(())
208                }
209            });
210
211            return Ok(());
212        }
213
214        // The same per-(arch, kver) archive can satisfy multiple file
215        // requests (e.g. `Kernel` and `BzImage` for the same x86_64 6.1
216        // archive), so dedupe download + extract on `(arch, kver)`.
217        let needed_archives: BTreeSet<(CommonArch, LinuxTestKernelVersion)> =
218            deps.keys().map(|(_, arch, kver)| (*arch, *kver)).collect();
219
220        let mut archives = BTreeMap::new();
221        for (arch, kver) in needed_archives {
222            let version = version.clone().expect("local requests handled above");
223            let arch_str = match arch {
224                CommonArch::X86_64 => "x86_64",
225                CommonArch::Aarch64 => "aarch64",
226            };
227            let kver_str = kver.artifact_tag();
228            let archive = ctx.reqv(|v| flowey_lib_common::download_gh_release::Request {
229                repo_owner: "microsoft".into(),
230                repo_name: "openvmm-deps".into(),
231                needs_auth: false,
232                tag: version.clone(),
233                file_name: format!("openvmm-test-linux-{kver_str}.{arch_str}.{version}.tar.gz"),
234                path: v,
235            });
236            archives.insert((arch, kver), archive);
237        }
238
239        let persistent_dir = ctx.persistent_dir();
240
241        ctx.emit_rust_step("unpack openvmm-test-linux archives", |ctx| {
242            let persistent_dir = persistent_dir.claim(ctx);
243            let archives = archives.claim(ctx);
244            let deps = deps.claim(ctx);
245            let version = version.clone().expect("local requests handled above");
246            move |rt| {
247                let persistent_dir = persistent_dir.map(|d| rt.read(d));
248
249                let mut extract_dirs = BTreeMap::new();
250                for (key, archive) in archives {
251                    let file = rt.read(archive);
252                    let dir = flowey_lib_common::_util::extract::extract_tar_gz_if_new(
253                        rt,
254                        persistent_dir.as_deref(),
255                        &file,
256                        &version,
257                    )?;
258                    extract_dirs.insert(key, dir);
259                }
260
261                for ((file, arch, kver), vars) in deps {
262                    let path = extract_dirs[&(arch, kver)].join(file.filename(arch));
263                    rt.write_all(vars, &path)
264                }
265
266                Ok(())
267            }
268        });
269
270        Ok(())
271    }
272}