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