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::{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 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 &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 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 new_etc
126 .remove_file_optional(".updated")
127 .context("Removing /etc/.updated from staged deployment")?;
128
129 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 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}