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::{compute_diff, merge, print_diff, traverse_etc};
18use rustix::fs::fsync;
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        &FileSystem::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    // Remove /etc/.updated from the new deployment so that ConditionNeedsUpdate=|/etc
123    // services (systemd-sysusers, systemd-tmpfiles) run on the first boot, mirroring
124    // what ostree does in sysroot_finalize_deployment.
125    new_etc
126        .remove_file_optional(".updated")
127        .context("Removing /etc/.updated from staged deployment")?;
128
129    // Unmount EROFS
130    drop(erofs_tmp_mnt);
131
132    let boot_dir = storage.require_boot_dir()?;
133
134    match booted_composefs.bootloader.kind()? {
135        BootloaderKind::GRUBClassic => match staged_composefs.boot_type {
136            BootType::Bls => {
137                let entries_dir = boot_dir.open_dir("loader")?;
138                rename_exchange_bls_entries(&entries_dir)?;
139            }
140            BootType::Uki => finalize_staged_grub_uki(boot_dir)?,
141        },
142
143        BootloaderKind::BLSCompatible => {
144            let entries_dir = boot_dir.open_dir("loader")?;
145            rename_exchange_bls_entries(&entries_dir)?;
146        }
147    };
148
149    // Now that we have successfully updated bootloader entires, we can GC the unreferenced ones
150    // We do not prune the composefs repository here though
151    composefs_gc(
152        storage,
153        booted_cfs,
154        GCOpts {
155            dry_run: false,
156            prune_repo: false,
157        },
158    )
159    .await?;
160
161    Ok(())
162}
163
164#[context("Grub: Finalizing staged UKI")]
165fn finalize_staged_grub_uki(boot_fd: &Dir) -> Result<()> {
166    let entries_dir = boot_fd.open_dir("grub2")?;
167    rename_exchange_user_cfg(&entries_dir)?;
168
169    let entries_dir = entries_dir.reopen_as_ownedfd()?;
170    fsync(entries_dir).context("fsync")?;
171
172    Ok(())
173}