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