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 rustix::fs::{AtFlags, RenameFlags, fsync, renameat_with};
8
9use crate::bootc_composefs::boot::{
10 BootType, FILENAME_PRIORITY_PRIMARY, FILENAME_PRIORITY_SECONDARY, primary_sort_key,
11 secondary_sort_key, type1_entry_conf_file_name,
12};
13use crate::bootc_composefs::status::{get_composefs_status, get_sorted_type1_boot_entries};
14use crate::composefs_consts::TYPE1_ENT_PATH_STAGED;
15use crate::spec::Bootloader;
16use crate::store::{BootedComposefs, Storage};
17use crate::{
18 bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_grub_uki_boot_entries},
19 composefs_consts::{
20 BOOT_LOADER_ENTRIES, STAGED_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_STAGED,
21 },
22 spec::BootOrder,
23};
24
25#[context("Atomically exchanging user.cfg")]
28pub(crate) fn rename_exchange_user_cfg(entries_dir: &Dir) -> Result<()> {
29 tracing::debug!("Atomically exchanging {USER_CFG_STAGED} and {USER_CFG}");
30 renameat_with(
31 &entries_dir,
32 USER_CFG_STAGED,
33 &entries_dir,
34 USER_CFG,
35 RenameFlags::EXCHANGE,
36 )
37 .context("renameat")?;
38
39 tracing::debug!("Removing {USER_CFG_STAGED}");
40 rustix::fs::unlinkat(&entries_dir, USER_CFG_STAGED, AtFlags::empty()).context("unlinkat")?;
41
42 tracing::debug!("Syncing to disk");
43 let entries_dir = entries_dir
44 .reopen_as_ownedfd()
45 .context("Reopening entries dir as owned fd")?;
46
47 fsync(entries_dir).context("fsync entries dir")?;
48
49 Ok(())
50}
51
52#[context("Atomically exchanging BLS entries")]
58pub(crate) fn rename_exchange_bls_entries(entries_dir: &Dir) -> Result<()> {
59 tracing::debug!("Atomically exchanging {STAGED_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}");
60 renameat_with(
61 &entries_dir,
62 STAGED_BOOT_LOADER_ENTRIES,
63 &entries_dir,
64 BOOT_LOADER_ENTRIES,
65 RenameFlags::EXCHANGE,
66 )
67 .context("renameat")?;
68
69 tracing::debug!("Removing {STAGED_BOOT_LOADER_ENTRIES}");
70 entries_dir
71 .remove_dir_all(STAGED_BOOT_LOADER_ENTRIES)
72 .context("Removing staged dir")?;
73
74 tracing::debug!("Syncing to disk");
75 let entries_dir = entries_dir
76 .reopen_as_ownedfd()
77 .context("Reopening as owned fd")?;
78
79 fsync(entries_dir).context("fsync")?;
80
81 Ok(())
82}
83
84#[context("Rolling back Grub UKI")]
85fn rollback_grub_uki_entries(boot_dir: &Dir) -> Result<()> {
86 let mut str = String::new();
87 let mut menuentries = get_sorted_grub_uki_boot_entries(&boot_dir, &mut str)
88 .context("Getting UKI boot entries")?;
89
90 assert!(menuentries.len() == 2);
92
93 let (first, second) = menuentries.split_at_mut(1);
94 std::mem::swap(&mut first[0], &mut second[0]);
95
96 let entries_dir = boot_dir.open_dir("grub2").context("Opening grub dir")?;
97
98 entries_dir
99 .atomic_replace_with(USER_CFG_STAGED, |f| -> std::io::Result<_> {
100 f.write_all(get_efi_uuid_source().as_bytes())?;
101
102 for entry in menuentries {
103 f.write_all(entry.to_string().as_bytes())?;
104 }
105
106 Ok(())
107 })
108 .with_context(|| format!("Writing to {USER_CFG_STAGED}"))?;
109
110 rename_exchange_user_cfg(&entries_dir)
111}
112
113#[context("Rolling back {bootloader} entries")]
127fn rollback_composefs_entries(boot_dir: &Dir, bootloader: Bootloader) -> Result<()> {
128 let mut all_configs = get_sorted_type1_boot_entries(&boot_dir, false)?;
130
131 assert!(all_configs.len() == 2);
133
134 let os_id = "bootc";
138
139 all_configs[0].sort_key = Some(primary_sort_key(os_id));
142 all_configs[1].sort_key = Some(secondary_sort_key(os_id));
145
146 boot_dir
148 .create_dir_all(TYPE1_ENT_PATH_STAGED)
149 .context("Creating staged dir")?;
150
151 let rollback_entries_dir = boot_dir
152 .open_dir(TYPE1_ENT_PATH_STAGED)
153 .context("Opening staged entries dir")?;
154
155 for cfg in all_configs {
157 let priority = if cfg.sort_key == Some(secondary_sort_key(os_id)) {
159 FILENAME_PRIORITY_SECONDARY
160 } else {
161 FILENAME_PRIORITY_PRIMARY
162 };
163
164 let file_name = type1_entry_conf_file_name(os_id, &cfg.version(), priority);
165
166 rollback_entries_dir
167 .atomic_write(&file_name, cfg.to_string())
168 .with_context(|| format!("Writing to {file_name}"))?;
169 }
170
171 let rollback_entries_dir = rollback_entries_dir
172 .reopen_as_ownedfd()
173 .context("Reopening as owned fd")?;
174
175 fsync(rollback_entries_dir).context("fsync")?;
177
178 let dir = boot_dir.open_dir("loader").context("Opening loader dir")?;
180
181 rename_exchange_bls_entries(&dir)
182}
183
184#[context("Rolling back composefs")]
185pub(crate) async fn composefs_rollback(
186 storage: &Storage,
187 booted_cfs: &BootedComposefs,
188) -> Result<()> {
189 const COMPOSEFS_ROLLBACK_JOURNAL_ID: &str = "6f5e4d3c2b1a0f9e8d7c6b5a4e3d2c1b0";
190
191 tracing::info!(
192 message_id = COMPOSEFS_ROLLBACK_JOURNAL_ID,
193 bootc.operation = "rollback",
194 "Starting composefs rollback operation"
195 );
196
197 let host = get_composefs_status(storage, booted_cfs).await?;
198
199 let new_spec = {
200 let mut new_spec = host.spec.clone();
201 new_spec.boot_order = new_spec.boot_order.swap();
202 new_spec
203 };
204
205 host.spec.verify_transition(&new_spec)?;
207
208 let reverting = new_spec.boot_order == BootOrder::Default;
209 if reverting {
210 println!("notice: Reverting queued rollback state");
211 }
212
213 let rollback_status = host
214 .status
215 .rollback
216 .ok_or_else(|| anyhow!("No rollback available"))?;
217
218 let Some(rollback_entry) = &rollback_status.composefs else {
222 anyhow::bail!("Rollback deployment not a composefs deployment")
223 };
224
225 let boot_dir = storage.require_boot_dir()?;
226
227 match &rollback_entry.bootloader {
228 Bootloader::Grub => match rollback_entry.boot_type {
229 BootType::Bls => {
230 rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?;
231 }
232 BootType::Uki => {
233 rollback_grub_uki_entries(boot_dir)?;
234 }
235 },
236
237 Bootloader::Systemd => {
238 rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?;
240 }
241
242 Bootloader::None => unreachable!("Checked at install time"),
243 }
244
245 if reverting {
246 println!("Next boot: current deployment");
247 } else {
248 println!("Next boot: rollback deployment");
249 }
250
251 Ok(())
252}