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 cfsctl::composefs;
15use composefs::generic_tree::{Directory, Stat};
16use etc_merge::{compute_diff, merge, print_diff, traverse_etc};
17use rustix::fs::{fsync, renameat};
18use rustix::path::Arg;
19
20use fn_error_context::context;
21
22pub(crate) async fn get_etc_diff(storage: &Storage, booted_cfs: &BootedComposefs) -> Result<()> {
23    let host = get_composefs_status(storage, booted_cfs).await?;
24    let booted_composefs = host.require_composefs_booted()?;
25
26    // Mount the booted EROFS image to get pristine etc
27    let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
28    let composefs_fd = mount_composefs_image(
29        &sysroot_fd,
30        &booted_composefs.verity,
31        booted_cfs.cmdline.allow_missing_fsverity,
32    )?;
33
34    let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
35
36    let pristine_etc =
37        Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
38    let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
39
40    let (pristine_files, current_files, _) = traverse_etc(&pristine_etc, &current_etc, None)?;
41    let diff = compute_diff(
42        &pristine_files,
43        &current_files,
44        &Directory::new(Stat::uninitialized()),
45    )?;
46
47    print_diff(&diff, &mut std::io::stdout());
48
49    Ok(())
50}
51
52pub(crate) async fn composefs_backend_finalize(
53    storage: &Storage,
54    booted_cfs: &BootedComposefs,
55) -> Result<()> {
56    const COMPOSEFS_FINALIZE_JOURNAL_ID: &str = "0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4";
57
58    tracing::info!(
59        message_id = COMPOSEFS_FINALIZE_JOURNAL_ID,
60        bootc.operation = "finalize",
61        bootc.current_deployment = booted_cfs.cmdline.digest,
62        "Starting composefs staged deployment finalization"
63    );
64
65    let host = get_composefs_status(storage, booted_cfs).await?;
66
67    let booted_composefs = host.require_composefs_booted()?;
68
69    let Some(staged_depl) = host.status.staged.as_ref() else {
70        tracing::info!(
71            message_id = COMPOSEFS_FINALIZE_JOURNAL_ID,
72            bootc.operation = "finalize",
73            "No staged deployment found"
74        );
75        return Ok(());
76    };
77
78    if staged_depl.download_only {
79        tracing::info!(
80            message_id = COMPOSEFS_FINALIZE_JOURNAL_ID,
81            bootc.operation = "finalize",
82            bootc.download_only = "true",
83            "Staged deployment is marked download only. Won't finalize"
84        );
85        return Ok(());
86    }
87
88    let staged_composefs = staged_depl.composefs.as_ref().ok_or(anyhow::anyhow!(
89        "Staged deployment is not a composefs deployment"
90    ))?;
91
92    // Mount the booted EROFS image to get pristine etc
93    let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
94    let composefs_fd = mount_composefs_image(
95        &sysroot_fd,
96        &booted_composefs.verity,
97        booted_cfs.cmdline.allow_missing_fsverity,
98    )?;
99
100    let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
101
102    // Perform the /etc merge
103    let pristine_etc =
104        Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
105    let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
106
107    let new_etc_path = Path::new(STATE_DIR_ABS)
108        .join(&staged_composefs.verity)
109        .join("etc");
110
111    let new_etc = Dir::open_ambient_dir(new_etc_path, ambient_authority())?;
112
113    let (pristine_files, current_files, new_files) =
114        traverse_etc(&pristine_etc, &current_etc, Some(&new_etc))?;
115
116    let new_files =
117        new_files.ok_or_else(|| anyhow::anyhow!("Failed to get dirtree for new etc"))?;
118
119    let diff = compute_diff(&pristine_files, &current_files, &new_files)?;
120    merge(&current_etc, &current_files, &new_etc, &new_files, &diff)?;
121
122    // Unmount EROFS
123    drop(erofs_tmp_mnt);
124
125    let boot_dir = storage.require_boot_dir()?;
126
127    let esp_mount = storage.require_esp()?;
128
129    // NOTE: Assuming here we won't have two bootloaders at the same time
130    match booted_composefs.bootloader {
131        Bootloader::Grub => match staged_composefs.boot_type {
132            BootType::Bls => {
133                let entries_dir = boot_dir.open_dir("loader")?;
134                rename_exchange_bls_entries(&entries_dir)?;
135            }
136            BootType::Uki => finalize_staged_grub_uki(&esp_mount.fd, boot_dir)?,
137        },
138
139        Bootloader::Systemd => {
140            if matches!(staged_composefs.boot_type, BootType::Uki) {
141                rename_staged_uki_entries(&esp_mount.fd)?;
142            }
143
144            let entries_dir = boot_dir.open_dir("loader")?;
145            rename_exchange_bls_entries(&entries_dir)?;
146        }
147
148        Bootloader::None => unreachable!("Checked at install time"),
149    };
150
151    Ok(())
152}
153
154#[context("Grub: Finalizing staged UKI")]
155fn finalize_staged_grub_uki(esp_mount: &Dir, boot_fd: &Dir) -> Result<()> {
156    rename_staged_uki_entries(esp_mount)?;
157
158    let entries_dir = boot_fd.open_dir("grub2")?;
159    rename_exchange_user_cfg(&entries_dir)?;
160
161    let entries_dir = entries_dir.reopen_as_ownedfd()?;
162    fsync(entries_dir).context("fsync")?;
163
164    Ok(())
165}
166
167#[context("Renaming staged UKI entries")]
168fn rename_staged_uki_entries(esp_mount: &Dir) -> Result<()> {
169    for entry in esp_mount.entries()? {
170        let entry = entry?;
171
172        let filename = entry.file_name();
173        let filename = filename.as_str()?;
174
175        if !filename.ends_with(".staged") {
176            continue;
177        }
178
179        renameat(
180            &esp_mount,
181            filename,
182            &esp_mount,
183            // SAFETY: We won't reach here if not for the above condition
184            filename.strip_suffix(".staged").unwrap(),
185        )
186        .context("Renaming {filename}")?;
187    }
188
189    let esp_mount = esp_mount.reopen_as_ownedfd()?;
190    fsync(esp_mount).context("fsync")?;
191
192    Ok(())
193}