bootc_lib/bootc_composefs/
finalize.rs

1use std::path::Path;
2
3use crate::bootc_composefs::boot::BootType;
4use crate::bootc_composefs::rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg};
5use crate::bootc_composefs::status::get_composefs_status;
6use crate::composefs_consts::STATE_DIR_ABS;
7use crate::spec::Bootloader;
8use crate::store::{BootedComposefs, Storage};
9use anyhow::{Context, Result};
10use bootc_initramfs_setup::mount_composefs_image;
11use bootc_mount::tempmount::TempMount;
12use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
13use cap_std_ext::dirext::CapStdExtDirExt;
14use etc_merge::{compute_diff, merge, print_diff, traverse_etc};
15use rustix::fs::{fsync, renameat};
16use rustix::path::Arg;
17
18use fn_error_context::context;
19
20pub(crate) async fn get_etc_diff(storage: &Storage, booted_cfs: &BootedComposefs) -> Result<()> {
21    let host = get_composefs_status(storage, booted_cfs).await?;
22    let booted_composefs = host.require_composefs_booted()?;
23
24    // Mount the booted EROFS image to get pristine etc
25    let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
26    let composefs_fd = mount_composefs_image(&sysroot_fd, &booted_composefs.verity, false)?;
27
28    let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
29
30    let pristine_etc =
31        Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
32    let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
33
34    let (pristine_files, current_files, _) = traverse_etc(&pristine_etc, &current_etc, None)?;
35    let diff = compute_diff(&pristine_files, &current_files)?;
36
37    print_diff(&diff, &mut std::io::stdout());
38
39    Ok(())
40}
41
42pub(crate) async fn composefs_backend_finalize(
43    storage: &Storage,
44    booted_cfs: &BootedComposefs,
45) -> Result<()> {
46    let host = get_composefs_status(storage, booted_cfs).await?;
47
48    let booted_composefs = host.require_composefs_booted()?;
49
50    let Some(staged_depl) = host.status.staged.as_ref() else {
51        tracing::debug!("No staged deployment found");
52        return Ok(());
53    };
54
55    let staged_composefs = staged_depl.composefs.as_ref().ok_or(anyhow::anyhow!(
56        "Staged deployment is not a composefs deployment"
57    ))?;
58
59    // Mount the booted EROFS image to get pristine etc
60    let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
61    let composefs_fd = mount_composefs_image(&sysroot_fd, &booted_composefs.verity, false)?;
62
63    let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
64
65    // Perform the /etc merge
66    let pristine_etc =
67        Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
68    let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
69
70    let new_etc_path = Path::new(STATE_DIR_ABS)
71        .join(&staged_composefs.verity)
72        .join("etc");
73
74    let new_etc = Dir::open_ambient_dir(new_etc_path, ambient_authority())?;
75
76    let (pristine_files, current_files, new_files) =
77        traverse_etc(&pristine_etc, &current_etc, Some(&new_etc))?;
78
79    let new_files = new_files.ok_or(anyhow::anyhow!("Failed to get dirtree for new etc"))?;
80
81    let diff = compute_diff(&pristine_files, &current_files)?;
82    merge(&current_etc, &current_files, &new_etc, &new_files, diff)?;
83
84    // Unmount EROFS
85    drop(erofs_tmp_mnt);
86
87    let boot_dir = storage.require_boot_dir()?;
88
89    let esp_mount = storage
90        .esp
91        .as_ref()
92        .ok_or_else(|| anyhow::anyhow!("ESP not found"))?;
93
94    // NOTE: Assuming here we won't have two bootloaders at the same time
95    match booted_composefs.bootloader {
96        Bootloader::Grub => match staged_composefs.boot_type {
97            BootType::Bls => {
98                let entries_dir = boot_dir.open_dir("loader")?;
99                rename_exchange_bls_entries(&entries_dir)?;
100            }
101            BootType::Uki => finalize_staged_grub_uki(&esp_mount.fd, boot_dir)?,
102        },
103
104        Bootloader::Systemd => {
105            if matches!(staged_composefs.boot_type, BootType::Uki) {
106                rename_staged_uki_entries(&esp_mount.fd)?;
107            }
108
109            let entries_dir = boot_dir.open_dir("loader")?;
110            rename_exchange_bls_entries(&entries_dir)?;
111        }
112    };
113
114    Ok(())
115}
116
117#[context("Grub: Finalizing staged UKI")]
118fn finalize_staged_grub_uki(esp_mount: &Dir, boot_fd: &Dir) -> Result<()> {
119    rename_staged_uki_entries(esp_mount)?;
120
121    let entries_dir = boot_fd.open_dir("grub2")?;
122    rename_exchange_user_cfg(&entries_dir)?;
123
124    let entries_dir = entries_dir.reopen_as_ownedfd()?;
125    fsync(entries_dir).context("fsync")?;
126
127    Ok(())
128}
129
130#[context("Renaming staged UKI entries")]
131fn rename_staged_uki_entries(esp_mount: &Dir) -> Result<()> {
132    for entry in esp_mount.entries()? {
133        let entry = entry?;
134
135        let filename = entry.file_name();
136        let filename = filename.as_str()?;
137
138        if !filename.ends_with(".staged") {
139            continue;
140        }
141
142        renameat(
143            &esp_mount,
144            filename,
145            &esp_mount,
146            // SAFETY: We won't reach here if not for the above condition
147            filename.strip_suffix(".staged").unwrap(),
148        )
149        .context("Renaming {filename}")?;
150    }
151
152    let esp_mount = esp_mount.reopen_as_ownedfd()?;
153    fsync(esp_mount).context("fsync")?;
154
155    Ok(())
156}