bootc_lib/bootc_composefs/
finalize.rs1use 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 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, ¤t_etc, new_etc)?;
46
47 let diff = compute_diff(
48 &pristine_files,
49 ¤t_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 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 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, ¤t_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, ¤t_files, &new_files)?;
126 merge(¤t_etc, ¤t_files, &new_etc, &new_files, &diff)?;
127
128 new_etc
132 .remove_file_optional(".updated")
133 .context("Removing /etc/.updated from staged deployment")?;
134
135 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 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}