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 `cca-v15` kernel (Linux 7.2.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 emulator VFIO device-assignment tests
34    /// need to exercise device-BAR P2P DMA, which predate the 6.18 test kernel.
35    CcaV15,
36    /// The x86_64-only SEV-SNP guest kernel used by the SNP Linux-direct IGVM.
37    SnpGuest,
38    /// Module-free x86_64 mshv root kernel, distinct from the child test kernel.
39    MshvTest,
40}
41
42impl LinuxTestKernelVersion {
43    /// The version string used in the openvmm-deps GitHub release artifact
44    /// filename (e.g. `"6.1"` for `openvmm-test-linux-6.1.<arch>.<ver>.tar.gz`).
45    pub fn artifact_tag(self) -> &'static str {
46        match self {
47            Self::Linux6_1 => "6.1",
48            Self::Linux6_18 => "6.18",
49            Self::CcaV15 => "cca-v15",
50            Self::SnpGuest => "snp-guest",
51            Self::MshvTest => "mshv-test",
52        }
53    }
54
55    /// Whether this kernel version is published for the given architecture.
56    ///
57    /// Most versions ship for both architectures; `cca-v15` is aarch64-only
58    /// and `snp-guest` and `mshv-test` are x86_64-only.
59    pub fn is_available_for(self, arch: CommonArch) -> bool {
60        match self {
61            Self::Linux6_1 | Self::Linux6_18 => true,
62            Self::CcaV15 => matches!(arch, CommonArch::Aarch64),
63            Self::SnpGuest | Self::MshvTest => matches!(arch, CommonArch::X86_64),
64        }
65    }
66}
67
68/// Which file to extract from a per-(arch, kver) `openvmm-test-linux` archive.
69#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
70pub enum OpenvmmTestKernelFile {
71    /// Primary kernel image: `vmlinux` on x86_64, `Image` on aarch64.
72    Kernel,
73    /// `bzImage`-format kernel image. Only available on x86_64.
74    BzImage,
75}
76
77impl OpenvmmTestKernelFile {
78    /// Whether this file is shipped in the archive for the given architecture.
79    pub fn is_available_for(self, arch: CommonArch) -> bool {
80        match self {
81            Self::Kernel => true,
82            Self::BzImage => matches!(arch, CommonArch::X86_64),
83        }
84    }
85
86    /// The filename of this file inside the per-(arch, kver) archive.
87    pub fn filename(self, arch: CommonArch) -> &'static str {
88        match self {
89            Self::Kernel => match arch {
90                CommonArch::X86_64 => "vmlinux",
91                CommonArch::Aarch64 => "Image",
92            },
93            Self::BzImage => "bzImage",
94        }
95    }
96}
97
98/// The default Linux test kernel version. Call sites that don't otherwise care
99/// which kernel they're using should pass this.
100pub const DEFAULT_LINUX_TEST_KERNEL_VERSION: LinuxTestKernelVersion =
101    LinuxTestKernelVersion::Linux6_18;
102
103/// The Linux test kernel used as the **L1 host image** for the aarch64 QEMU-TCG
104/// emulated tests. Unlike [`DEFAULT_LINUX_TEST_KERNEL_VERSION`], this must ship
105/// the P2PDMA / vfio-dmabuf / iommufd config
106/// ([`LinuxTestKernelVersion::CcaV15`], Linux 7.2.0-rc1) so emulated VFIO
107/// device-assignment tests can exercise device-BAR peer-to-peer DMA. Aarch64
108/// only.
109pub const CCA_LINUX_TEST_KERNEL_VERSION: LinuxTestKernelVersion = LinuxTestKernelVersion::CcaV15;
110
111/// The x86_64 SEV-SNP guest kernel used by the SNP Linux-direct IGVM.
112pub const SNP_GUEST_LINUX_TEST_KERNEL_VERSION: LinuxTestKernelVersion =
113    LinuxTestKernelVersion::SnpGuest;
114
115flowey_config! {
116    /// Config for the resolve_openvmm_test_linux_kernel node.
117    pub struct Config {
118        /// Specify version of the github release to pull from
119        pub version: Option<String>,
120        /// Override the mshv-test release independently of other test kernels.
121        pub mshv_version: Option<String>,
122        /// Use locally downloaded openvmm-test-linux contents, keyed by
123        /// (architecture, kernel version)
124        pub local_paths: BTreeMap<(CommonArch, LinuxTestKernelVersion), ConfigVar<PathBuf>>,
125    }
126}
127
128flowey_request! {
129    pub enum Request {
130        /// Get the path to a specific file from the per-(arch, kver) archive.
131        Get(
132            OpenvmmTestKernelFile,
133            CommonArch,
134            LinuxTestKernelVersion,
135            WriteVar<PathBuf>,
136        ),
137    }
138}
139
140new_flow_node_with_config!(struct Node);
141
142impl FlowNodeWithConfig for Node {
143    type Request = Request;
144    type Config = Config;
145
146    fn imports(ctx: &mut ImportCtx<'_>) {
147        ctx.import::<flowey_lib_common::install_dist_pkg::Node>();
148        ctx.import::<flowey_lib_common::download_gh_release::Node>();
149    }
150
151    fn emit(
152        config: Config,
153        requests: Vec<Self::Request>,
154        ctx: &mut NodeCtx<'_>,
155    ) -> anyhow::Result<()> {
156        let Config {
157            version,
158            mshv_version,
159            local_paths,
160        } = config;
161        let mut deps: BTreeMap<
162            (OpenvmmTestKernelFile, CommonArch, LinuxTestKernelVersion),
163            Vec<WriteVar<PathBuf>>,
164        > = BTreeMap::new();
165
166        for req in requests {
167            match req {
168                Request::Get(file, arch, kver, var) => {
169                    if !kver.is_available_for(arch) {
170                        anyhow::bail!(
171                            "test kernel {:?} is not published for {arch:?}",
172                            kver.artifact_tag()
173                        );
174                    }
175                    if !file.is_available_for(arch) {
176                        anyhow::bail!(
177                            "{file:?} is not available in the openvmm-test-linux archive for {arch:?}"
178                        );
179                    }
180                    deps.entry((file, arch, kver)).or_default().push(var);
181                }
182            }
183        }
184
185        if version.is_some() && !local_paths.is_empty() {
186            anyhow::bail!("Cannot specify both Version and LocalPath requests");
187        }
188
189        if version.is_none() && mshv_version.is_none() && local_paths.is_empty() {
190            anyhow::bail!("Must specify a Version or LocalPath request");
191        }
192
193        // -- end of req processing -- //
194
195        if deps.is_empty() {
196            return Ok(());
197        }
198
199        if !local_paths.is_empty() {
200            ctx.emit_rust_step("use local openvmm-test-linux", |ctx| {
201                let deps = deps.claim(ctx);
202                let local_paths: BTreeMap<_, _> = local_paths
203                    .into_iter()
204                    .map(|(key, var)| (key, var.claim(ctx)))
205                    .collect();
206                move |rt| {
207                    let resolved_paths: BTreeMap<(CommonArch, LinuxTestKernelVersion), PathBuf> =
208                        local_paths
209                            .into_iter()
210                            .map(|(key, var)| (key, rt.read(var)))
211                            .collect();
212
213                    for ((file, arch, kver), vars) in deps {
214                        let base_dir = resolved_paths.get(&(arch, kver)).ok_or_else(|| {
215                            anyhow::anyhow!("No local path specified for ({:?}, {:?})", arch, kver)
216                        })?;
217                        let path = base_dir.join(file.filename(arch));
218                        rt.write_all(vars, &path)
219                    }
220
221                    Ok(())
222                }
223            });
224
225            return Ok(());
226        }
227
228        // The same per-(arch, kver) archive can satisfy multiple file
229        // requests (e.g. `Kernel` and `BzImage` for the same x86_64 6.1
230        // archive), so dedupe download + extract on `(arch, kver)`.
231        let needed_archives: BTreeSet<(CommonArch, LinuxTestKernelVersion)> =
232            deps.keys().map(|(_, arch, kver)| (*arch, *kver)).collect();
233        let mut archives = BTreeMap::new();
234        for (arch, kver) in needed_archives {
235            let version = if kver == LinuxTestKernelVersion::MshvTest {
236                mshv_version.as_ref().or(version.as_ref())
237            } else {
238                version.as_ref()
239            }
240            .with_context(|| format!("missing openvmm-test-linux version for {kver:?}"))?
241            .clone();
242            let arch_str = match arch {
243                CommonArch::X86_64 => "x86_64",
244                CommonArch::Aarch64 => "aarch64",
245            };
246            let kver_str = kver.artifact_tag();
247            let archive = ctx.reqv(|v| flowey_lib_common::download_gh_release::Request {
248                repo_owner: crate::common::OPENVMM_GITHUB_OWNER.into(),
249                repo_name: crate::common::OPENVMM_DEPS_GITHUB_REPO.into(),
250                needs_auth: false,
251                tag: version.clone(),
252                file_name: format!("openvmm-test-linux-{kver_str}.{arch_str}.{version}.tar.gz"),
253                path: v,
254            });
255            archives.insert((arch, kver), (archive, version));
256        }
257
258        let persistent_dir = ctx.persistent_dir();
259
260        ctx.emit_rust_step("unpack openvmm-test-linux archives", |ctx| {
261            let persistent_dir = persistent_dir.claim(ctx);
262            let archives: BTreeMap<_, _> = archives
263                .into_iter()
264                .map(|(key, (archive, version))| (key, (archive.claim(ctx), version)))
265                .collect();
266            let deps = deps.claim(ctx);
267            move |rt| {
268                let persistent_dir = persistent_dir.map(|d| rt.read(d));
269
270                let mut extract_dirs = BTreeMap::new();
271                for (key, (archive, version)) in archives {
272                    let file = rt.read(archive);
273                    let dir = flowey_lib_common::_util::extract::extract_tar_gz_if_new(
274                        rt,
275                        persistent_dir.as_deref(),
276                        &file,
277                        &version,
278                    )?;
279                    extract_dirs.insert(key, dir);
280                }
281
282                for ((file, arch, kver), vars) in deps {
283                    let path = extract_dirs
284                        .get(&(arch, kver))
285                        .with_context(|| {
286                            format!(
287                                "missing extracted openvmm-test-linux archive for {arch:?} {kver:?}"
288                            )
289                        })?
290                        .join(file.filename(arch));
291                    rt.write_all(vars, &path)
292                }
293
294                Ok(())
295            }
296        });
297
298        Ok(())
299    }
300}