Skip to main content

bootc_lib/bootc_composefs/
finalize.rs

1use std::path::Path;
2
3use crate::bootc_composefs::boot::BootType;
4use crate::bootc_composefs::gc::{GCOpts, composefs_gc};
5use crate::bootc_composefs::rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg};
6use crate::bootc_composefs::status::get_composefs_status;
7use crate::composefs_consts::STATE_DIR_ABS;
8use crate::spec::BootloaderKind;
9use crate::store::{BootedComposefs, Storage};
10use anyhow::{Context, Result};
11use bootc_initramfs_setup::mount_composefs_image;
12use bootc_mount::tempmount::TempMount;
13use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
14use cap_std_ext::dirext::CapStdExtDirExt;
15use composefs::generic_tree::{FileSystem, Stat};
16use composefs_ctl::composefs;
17use etc_merge::{Diff, compute_diff, merge, traverse_etc};
18use rustix::fs::fsync;
19
20use fn_error_context::context;
21
22pub(crate) async fn get_etc_diff(
23    storage: &Storage,
24    booted_cfs: &BootedComposefs,
25    new_etc: Option<&Dir>,
26) -> Result<Diff> {
27    let host = get_composefs_status(storage, booted_cfs).await?;
28    let booted_composefs = host.require_composefs_booted()?;
29
30    // Mount the booted EROFS image to get pristine etc
31    let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
32    let composefs_fd = mount_composefs_image(
33        &sysroot_fd,
34        &booted_composefs.verity,
35        booted_cfs.cmdline.allow_missing_fsverity,
36    )?;
37
38    let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
39
40    let pristine_etc =
41        Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
42    let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
43
44    let (pristine_files, current_files, new_etc_files) =
45        traverse_etc(&pristine_etc, &current_etc, new_etc)?;
46
47    let diff = compute_diff(
48        &pristine_files,
49        &current_files,
50        &new_etc_files
51            .as_ref()
52            .unwrap_or(&FileSystem::new(Stat::uninitialized())),
53    )?;
54
55    Ok(diff)
56}
57
58pub(crate) async fn composefs_backend_finalize(
59    storage: &Storage,
60    booted_cfs: &BootedComposefs,
61) -> Result<()> {
62    const COMPOSEFS_FINALIZE_JOURNAL_ID: &str = "0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4";
63
64    tracing::info!(
65        message_id = COMPOSEFS_FINALIZE_JOURNAL_ID,
66        bootc.operation = "finalize",
67        bootc.current_deployment = booted_cfs.cmdline.digest,
68        "Starting composefs staged deployment finalization"
69    );
70
71    let host = get_composefs_status(storage, booted_cfs).await?;
72
73    let booted_composefs = host.require_composefs_booted()?;
74
75    let Some(staged_depl) = host.status.staged.as_ref() else {
76        tracing::info!(
77            message_id = COMPOSEFS_FINALIZE_JOURNAL_ID,
78            bootc.operation = "finalize",
79            "No staged deployment found"
80        );
81        return Ok(());
82    };
83
84    if staged_depl.download_only {
85        tracing::info!(
86            message_id = COMPOSEFS_FINALIZE_JOURNAL_ID,
87            bootc.operation = "finalize",
88            bootc.download_only = "true",
89            "Staged deployment is marked download only. Won't finalize"
90        );
91        return Ok(());
92    }
93
94    let staged_composefs = staged_depl.composefs.as_ref().ok_or(anyhow::anyhow!(
95        "Staged deployment is not a composefs deployment"
96    ))?;
97
98    // Mount the booted EROFS image to get pristine etc
99    let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
100    let composefs_fd = mount_composefs_image(
101        &sysroot_fd,
102        &booted_composefs.verity,
103        booted_cfs.cmdline.allow_missing_fsverity,
104    )?;
105
106    let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
107
108    // Perform the /etc merge
109    let pristine_etc =
110        Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
111    let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
112
113    let new_etc_path = Path::new(STATE_DIR_ABS)
114        .join(&staged_composefs.verity)
115        .join("etc");
116
117    let new_etc = Dir::open_ambient_dir(new_etc_path, ambient_authority())?;
118
119    let (pristine_files, current_files, new_files) =
120        traverse_etc(&pristine_etc, &current_etc, Some(&new_etc))?;
121
122    let new_files =
123        new_files.ok_or_else(|| anyhow::anyhow!("Failed to get dirtree for new etc"))?;
124
125    let diff = compute_diff(&pristine_files, &current_files, &new_files)?;
126    merge(&current_etc, &current_files, &new_etc, &new_files, &diff)?;
127
128    // Remove /etc/.updated from the new deployment so that ConditionNeedsUpdate=|/etc
129    // services (systemd-sysusers, systemd-tmpfiles) run on the first boot, mirroring
130    // what ostree does in sysroot_finalize_deployment.
131    new_etc
132        .remove_file_optional(".updated")
133        .context("Removing /etc/.updated from staged deployment")?;
134
135    // Unmount EROFS
136    drop(erofs_tmp_mnt);
137
138    let boot_dir = storage.require_boot_dir()?;
139
140    match booted_composefs.bootloader.kind()? {
141        BootloaderKind::GRUBClassic => match staged_composefs.boot_type {
142            BootType::Bls => {
143                let entries_dir = boot_dir.open_dir("loader")?;
144                rename_exchange_bls_entries(&entries_dir)?;
145            }
146            BootType::Uki => finalize_staged_grub_uki(boot_dir)?,
147        },
148
149        BootloaderKind::BLSCompatible => {
150            let entries_dir = boot_dir.open_dir("loader")?;
151            rename_exchange_bls_entries(&entries_dir)?;
152        }
153    };
154
155    // Now that we have successfully updated bootloader entires, we can GC the unreferenced ones
156    // We do not prune the composefs repository here though
157    composefs_gc(
158        storage,
159        booted_cfs,
160        GCOpts {
161            dry_run: false,
162            prune_repo: false,
163        },
164    )
165    .await?;
166
167    Ok(())
168}
169
170#[context("Grub: Finalizing staged UKI")]
171fn finalize_staged_grub_uki(boot_fd: &Dir) -> Result<()> {
172    let entries_dir = boot_fd.open_dir("grub2")?;
173    rename_exchange_user_cfg(&entries_dir)?;
174
175    let entries_dir = entries_dir.reopen_as_ownedfd()?;
176    fsync(entries_dir).context("fsync")?;
177
178    Ok(())
179}