bootc_lib/bootc_composefs/
rollback.rs1use std::{io::Write, process::Command};
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::aboot;
11use crate::bootc_composefs::boot::{
12 BootType, FILENAME_PRIORITY_PRIMARY, FILENAME_PRIORITY_SECONDARY, STAGED_GLOBAL_UKI_ADDONS_DIR,
13 os_id_from_sort_key, primary_sort_key, secondary_sort_key, type1_entry_conf_file_name,
14};
15use crate::bootc_composefs::status::{get_composefs_status, get_sorted_type1_boot_entries};
16use crate::composefs_consts::{
17 COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, TYPE1_ENT_PATH_STAGED,
18};
19use crate::deploy::ROLLBACK_JOURNAL_ID;
20use crate::spec::{Bootloader, BootloaderKind, Host};
21use crate::store::{BootedComposefs, Storage};
22use crate::{
23 bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_grub_uki_boot_entries},
24 composefs_consts::{
25 BOOT_LOADER_ENTRIES, STAGED_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_STAGED,
26 },
27 spec::BootOrder,
28};
29
30#[context("Atomically exchanging user.cfg")]
33pub(crate) fn rename_exchange_user_cfg(grub2_dir: &Dir) -> Result<()> {
34 tracing::debug!("Atomically exchanging {USER_CFG_STAGED} and {USER_CFG}");
35 renameat_with(
36 &grub2_dir,
37 USER_CFG_STAGED,
38 &grub2_dir,
39 USER_CFG,
40 RenameFlags::EXCHANGE,
41 )
42 .context("renameat")?;
43
44 tracing::debug!("Removing {USER_CFG_STAGED}");
45 rustix::fs::unlinkat(&grub2_dir, USER_CFG_STAGED, AtFlags::empty()).context("unlinkat")?;
46
47 tracing::debug!("Syncing to disk");
48 let entries_dir = grub2_dir
49 .reopen_as_ownedfd()
50 .context("Reopening entries dir as owned fd")?;
51
52 fsync(entries_dir).context("fsync entries dir")?;
53
54 Ok(())
55}
56
57#[context("Atomically exchanging BLS entries")]
63pub(crate) fn rename_exchange_bls_entries(entries_dir: &Dir) -> Result<()> {
64 tracing::debug!("Atomically exchanging {STAGED_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}");
65 renameat_with(
66 &entries_dir,
67 STAGED_BOOT_LOADER_ENTRIES,
68 &entries_dir,
69 BOOT_LOADER_ENTRIES,
70 RenameFlags::EXCHANGE,
71 )
72 .context("renameat")?;
73
74 tracing::debug!("Removing {STAGED_BOOT_LOADER_ENTRIES}");
75 entries_dir
76 .remove_dir_all(STAGED_BOOT_LOADER_ENTRIES)
77 .context("Removing staged dir")?;
78
79 tracing::debug!("Syncing to disk");
80 let entries_dir = entries_dir
81 .reopen_as_ownedfd()
82 .context("Reopening as owned fd")?;
83
84 fsync(entries_dir).context("fsync")?;
85
86 Ok(())
87}
88
89#[context("Rolling back Grub UKI")]
90fn rollback_grub_uki_entries(boot_dir: &Dir) -> Result<()> {
91 let mut str = String::new();
92 let mut menuentries = get_sorted_grub_uki_boot_entries(&boot_dir, &mut str)
93 .context("Getting UKI boot entries")?;
94
95 assert!(menuentries.len() == 2);
97
98 let (first, second) = menuentries.split_at_mut(1);
99 std::mem::swap(&mut first[0], &mut second[0]);
100
101 let entries_dir = boot_dir.open_dir("grub2").context("Opening grub dir")?;
102
103 entries_dir
104 .atomic_replace_with(USER_CFG_STAGED, |f| -> std::io::Result<_> {
105 f.write_all(get_efi_uuid_source().as_bytes())?;
106
107 for entry in menuentries {
108 f.write_all(entry.to_string().as_bytes())?;
109 }
110
111 Ok(())
112 })
113 .with_context(|| format!("Writing to {USER_CFG_STAGED}"))?;
114
115 rename_exchange_user_cfg(&entries_dir)
116}
117
118#[context("Rolling back {bootloader} entries")]
132fn rollback_composefs_entries(host: &Host, boot_dir: &Dir, bootloader: Bootloader) -> Result<()> {
133 let mut all_configs = get_sorted_type1_boot_entries(&boot_dir, false)?;
135
136 assert!(all_configs.len() == 2);
138
139 let os_id = all_configs
142 .iter()
143 .find_map(|cfg| os_id_from_sort_key(cfg.sort_key.as_deref()?))
144 .unwrap_or("bootc")
145 .to_owned();
146 let os_id = os_id.as_str();
147
148 all_configs[0].sort_key = Some(primary_sort_key(os_id));
153 all_configs[1].sort_key = Some(secondary_sort_key(os_id));
156
157 boot_dir
162 .remove_all_optional(TYPE1_ENT_PATH_STAGED)
163 .context("Removing staged entries")?;
164
165 if let Some(staged) = &host.status.staged {
166 tracing::info!(
167 message_id = ROLLBACK_JOURNAL_ID,
168 "Removing currently staged composefs deployment {}",
169 staged.composefs.as_ref().unwrap().verity
172 );
173
174 let transient_dir =
175 Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
176 .context("Opening transient dir")?;
177
178 transient_dir
179 .remove_file(COMPOSEFS_STAGED_DEPLOYMENT_FNAME)
180 .context("Removing staged deployment file")?;
181 }
182
183 boot_dir
185 .create_dir_all(TYPE1_ENT_PATH_STAGED)
186 .context("Creating staged dir")?;
187
188 let rollback_entries_dir = boot_dir
189 .open_dir(TYPE1_ENT_PATH_STAGED)
190 .context("Opening staged entries dir")?;
191
192 for cfg in all_configs {
194 let priority = if cfg.sort_key == Some(secondary_sort_key(os_id)) {
196 FILENAME_PRIORITY_SECONDARY
197 } else {
198 FILENAME_PRIORITY_PRIMARY
199 };
200
201 let file_name = type1_entry_conf_file_name(os_id, &cfg.version(), priority);
202
203 rollback_entries_dir
204 .atomic_write(&file_name, cfg.to_string())
205 .with_context(|| format!("Writing to {file_name}"))?;
206 }
207
208 let rollback_entries_dir = rollback_entries_dir
209 .reopen_as_ownedfd()
210 .context("Reopening as owned fd")?;
211
212 fsync(rollback_entries_dir).context("fsync")?;
214
215 let dir = boot_dir.open_dir("loader").context("Opening loader dir")?;
217
218 rename_exchange_bls_entries(&dir)
219}
220
221#[context("Rolling back composefs")]
222pub(crate) async fn composefs_rollback(
223 storage: &Storage,
224 booted_cfs: &BootedComposefs,
225) -> Result<()> {
226 const COMPOSEFS_ROLLBACK_JOURNAL_ID: &str = "6f5e4d3c2b1a0f9e8d7c6b5a4e3d2c1b0";
227
228 tracing::info!(
229 message_id = COMPOSEFS_ROLLBACK_JOURNAL_ID,
230 bootc.operation = "rollback",
231 "Starting composefs rollback operation"
232 );
233
234 let host = get_composefs_status(storage, booted_cfs).await?;
235
236 let new_spec = {
237 let mut new_spec = host.spec.clone();
238 new_spec.boot_order = new_spec.boot_order.swap();
239 new_spec
240 };
241
242 host.spec.verify_transition(&new_spec)?;
244
245 let reverting = new_spec.boot_order == BootOrder::Default;
246 if reverting {
247 println!("notice: Reverting queued rollback state");
248 }
249
250 let rollback_status = host
251 .status
252 .rollback
253 .as_ref()
254 .ok_or_else(|| anyhow!("No rollback available"))?;
255
256 let Some(rollback_entry) = &rollback_status.composefs else {
257 anyhow::bail!("Rollback deployment not a composefs deployment")
258 };
259
260 if host.require_composefs_booted()?.boot_type == BootType::Aboot {
261 anyhow::ensure!(
262 rollback_entry.boot_type == BootType::Aboot,
263 "Rollback deployment is not aboot"
264 );
265 let mut state = aboot::AbootState::open(&storage.physical_root)?;
266 state.discard_pending()?;
267
268 let status = Command::new("aboot-deploy")
269 .arg("--rollback")
270 .status()
271 .context("Running aboot-deploy --rollback")?;
272 anyhow::ensure!(status.success(), "aboot-deploy exited with status {status}");
273
274 if reverting {
275 state.clear_rollback()?;
276 } else {
277 state.queue_rollback(&rollback_entry.verity)?;
278 }
279 } else {
280 let boot_dir = storage.require_boot_dir()?;
281
282 match &rollback_entry.bootloader.kind()? {
283 BootloaderKind::GRUBClassic => match rollback_entry.boot_type {
284 BootType::Bls => {
285 rollback_composefs_entries(&host, boot_dir, rollback_entry.bootloader.clone())?;
286 }
287 BootType::Uki => {
288 rollback_grub_uki_entries(boot_dir)?;
289 }
290 BootType::Aboot => anyhow::bail!("Unexpected aboot rollback deployment"),
291 },
292
293 BootloaderKind::BLSCompatible => {
294 let boot_mount = storage.require_boot_mount()?;
299
300 boot_mount
301 .fd
302 .remove_all_optional(STAGED_GLOBAL_UKI_ADDONS_DIR)
303 .context("Removing staged Global UKI Addons")?;
304
305 rollback_composefs_entries(&host, boot_dir, rollback_entry.bootloader.clone())?;
307 }
308 }
309 }
310
311 if reverting {
312 println!("Next boot: current deployment");
313 } else {
314 println!("Next boot: rollback deployment");
315 }
316
317 Ok(())
318}