bootc_lib/bootc_composefs/
rollback.rs1use std::io::Write;
2
3use anyhow::{Context, Result, anyhow};
4use cap_std_ext::cap_std::fs::Dir;
5use cap_std_ext::dirext::CapStdExtDirExt;
6use fn_error_context::context;
7use ocidir::cap_std::ambient_authority;
8use rustix::fs::{AtFlags, RenameFlags, fsync, renameat_with};
9
10use crate::bootc_composefs::boot::{
11 BootType, FILENAME_PRIORITY_PRIMARY, FILENAME_PRIORITY_SECONDARY, primary_sort_key,
12 secondary_sort_key, type1_entry_conf_file_name,
13};
14use crate::bootc_composefs::status::{get_composefs_status, get_sorted_type1_boot_entries};
15use crate::composefs_consts::{
16 COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, TYPE1_ENT_PATH_STAGED,
17};
18use crate::deploy::ROLLBACK_JOURNAL_ID;
19use crate::spec::{Bootloader, BootloaderKind, Host};
20use crate::store::{BootedComposefs, Storage};
21use crate::{
22 bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_grub_uki_boot_entries},
23 composefs_consts::{
24 BOOT_LOADER_ENTRIES, STAGED_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_STAGED,
25 },
26 spec::BootOrder,
27};
28
29#[context("Atomically exchanging user.cfg")]
32pub(crate) fn rename_exchange_user_cfg(grub2_dir: &Dir) -> Result<()> {
33 tracing::debug!("Atomically exchanging {USER_CFG_STAGED} and {USER_CFG}");
34 renameat_with(
35 &grub2_dir,
36 USER_CFG_STAGED,
37 &grub2_dir,
38 USER_CFG,
39 RenameFlags::EXCHANGE,
40 )
41 .context("renameat")?;
42
43 tracing::debug!("Removing {USER_CFG_STAGED}");
44 rustix::fs::unlinkat(&grub2_dir, USER_CFG_STAGED, AtFlags::empty()).context("unlinkat")?;
45
46 tracing::debug!("Syncing to disk");
47 let entries_dir = grub2_dir
48 .reopen_as_ownedfd()
49 .context("Reopening entries dir as owned fd")?;
50
51 fsync(entries_dir).context("fsync entries dir")?;
52
53 Ok(())
54}
55
56#[context("Atomically exchanging BLS entries")]
62pub(crate) fn rename_exchange_bls_entries(entries_dir: &Dir) -> Result<()> {
63 tracing::debug!("Atomically exchanging {STAGED_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}");
64 renameat_with(
65 &entries_dir,
66 STAGED_BOOT_LOADER_ENTRIES,
67 &entries_dir,
68 BOOT_LOADER_ENTRIES,
69 RenameFlags::EXCHANGE,
70 )
71 .context("renameat")?;
72
73 tracing::debug!("Removing {STAGED_BOOT_LOADER_ENTRIES}");
74 entries_dir
75 .remove_dir_all(STAGED_BOOT_LOADER_ENTRIES)
76 .context("Removing staged dir")?;
77
78 tracing::debug!("Syncing to disk");
79 let entries_dir = entries_dir
80 .reopen_as_ownedfd()
81 .context("Reopening as owned fd")?;
82
83 fsync(entries_dir).context("fsync")?;
84
85 Ok(())
86}
87
88#[context("Rolling back Grub UKI")]
89fn rollback_grub_uki_entries(boot_dir: &Dir) -> Result<()> {
90 let mut str = String::new();
91 let mut menuentries = get_sorted_grub_uki_boot_entries(&boot_dir, &mut str)
92 .context("Getting UKI boot entries")?;
93
94 assert!(menuentries.len() == 2);
96
97 let (first, second) = menuentries.split_at_mut(1);
98 std::mem::swap(&mut first[0], &mut second[0]);
99
100 let entries_dir = boot_dir.open_dir("grub2").context("Opening grub dir")?;
101
102 entries_dir
103 .atomic_replace_with(USER_CFG_STAGED, |f| -> std::io::Result<_> {
104 f.write_all(get_efi_uuid_source().as_bytes())?;
105
106 for entry in menuentries {
107 f.write_all(entry.to_string().as_bytes())?;
108 }
109
110 Ok(())
111 })
112 .with_context(|| format!("Writing to {USER_CFG_STAGED}"))?;
113
114 rename_exchange_user_cfg(&entries_dir)
115}
116
117#[context("Rolling back {bootloader} entries")]
131fn rollback_composefs_entries(host: &Host, boot_dir: &Dir, bootloader: Bootloader) -> Result<()> {
132 let mut all_configs = get_sorted_type1_boot_entries(&boot_dir, false)?;
134
135 assert!(all_configs.len() == 2);
137
138 let os_id = "bootc";
142
143 all_configs[0].sort_key = Some(primary_sort_key(os_id));
146 all_configs[1].sort_key = Some(secondary_sort_key(os_id));
149
150 boot_dir
155 .remove_all_optional(TYPE1_ENT_PATH_STAGED)
156 .context("Removing staged entries")?;
157
158 if let Some(staged) = &host.status.staged {
159 tracing::info!(
160 message_id = ROLLBACK_JOURNAL_ID,
161 "Removing currently staged composefs deployment {}",
162 staged.composefs.as_ref().unwrap().verity
165 );
166
167 let transient_dir =
168 Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
169 .context("Opening transient dir")?;
170
171 transient_dir
172 .remove_file(COMPOSEFS_STAGED_DEPLOYMENT_FNAME)
173 .context("Removing staged deployment file")?;
174 }
175
176 boot_dir
178 .create_dir_all(TYPE1_ENT_PATH_STAGED)
179 .context("Creating staged dir")?;
180
181 let rollback_entries_dir = boot_dir
182 .open_dir(TYPE1_ENT_PATH_STAGED)
183 .context("Opening staged entries dir")?;
184
185 for cfg in all_configs {
187 let priority = if cfg.sort_key == Some(secondary_sort_key(os_id)) {
189 FILENAME_PRIORITY_SECONDARY
190 } else {
191 FILENAME_PRIORITY_PRIMARY
192 };
193
194 let file_name = type1_entry_conf_file_name(os_id, &cfg.version(), priority);
195
196 rollback_entries_dir
197 .atomic_write(&file_name, cfg.to_string())
198 .with_context(|| format!("Writing to {file_name}"))?;
199 }
200
201 let rollback_entries_dir = rollback_entries_dir
202 .reopen_as_ownedfd()
203 .context("Reopening as owned fd")?;
204
205 fsync(rollback_entries_dir).context("fsync")?;
207
208 let dir = boot_dir.open_dir("loader").context("Opening loader dir")?;
210
211 rename_exchange_bls_entries(&dir)
212}
213
214#[context("Rolling back composefs")]
215pub(crate) async fn composefs_rollback(
216 storage: &Storage,
217 booted_cfs: &BootedComposefs,
218) -> Result<()> {
219 const COMPOSEFS_ROLLBACK_JOURNAL_ID: &str = "6f5e4d3c2b1a0f9e8d7c6b5a4e3d2c1b0";
220
221 tracing::info!(
222 message_id = COMPOSEFS_ROLLBACK_JOURNAL_ID,
223 bootc.operation = "rollback",
224 "Starting composefs rollback operation"
225 );
226
227 let host = get_composefs_status(storage, booted_cfs).await?;
228
229 let new_spec = {
230 let mut new_spec = host.spec.clone();
231 new_spec.boot_order = new_spec.boot_order.swap();
232 new_spec
233 };
234
235 host.spec.verify_transition(&new_spec)?;
237
238 let reverting = new_spec.boot_order == BootOrder::Default;
239 if reverting {
240 println!("notice: Reverting queued rollback state");
241 }
242
243 let rollback_status = host
244 .status
245 .rollback
246 .as_ref()
247 .ok_or_else(|| anyhow!("No rollback available"))?;
248
249 let Some(rollback_entry) = &rollback_status.composefs else {
250 anyhow::bail!("Rollback deployment not a composefs deployment")
251 };
252
253 let boot_dir = storage.require_boot_dir()?;
254
255 match &rollback_entry.bootloader.kind()? {
256 BootloaderKind::GRUBClassic => match rollback_entry.boot_type {
257 BootType::Bls => {
258 rollback_composefs_entries(&host, boot_dir, rollback_entry.bootloader.clone())?;
259 }
260 BootType::Uki => {
261 rollback_grub_uki_entries(boot_dir)?;
262 }
263 },
264
265 BootloaderKind::BLSCompatible => {
266 rollback_composefs_entries(&host, boot_dir, rollback_entry.bootloader.clone())?;
268 }
269 }
270
271 if reverting {
272 println!("Next boot: current deployment");
273 } else {
274 println!("Next boot: rollback deployment");
275 }
276
277 Ok(())
278}