bootc_lib/bootc_composefs/
finalize.rs1use 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 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, ¤t_etc, None)?;
41 let diff = compute_diff(
42 &pristine_files,
43 ¤t_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 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 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, ¤t_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, ¤t_files, &new_files)?;
120 merge(¤t_etc, ¤t_files, &new_etc, &new_files, &diff)?;
121
122 drop(erofs_tmp_mnt);
124
125 let boot_dir = storage.require_boot_dir()?;
126
127 let esp_mount = storage.require_esp()?;
128
129 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 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}