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")]
118fn rollback_composefs_entries(boot_dir: &Dir, bootloader: Bootloader) -> Result<()> {
119 use crate::bootc_composefs::state::get_booted_bls;
120
121 let mut all_configs = get_sorted_type1_boot_entries(&boot_dir, false)?;
123
124 assert!(all_configs.len() == 2);
126
127 let booted_bls = get_booted_bls(&boot_dir)?;
129 let booted_verity = booted_bls.get_verity()?;
130
131 let os_id = "bootc";
135
136 for cfg in &mut all_configs {
137 let cfg_verity = cfg.get_verity()?;
138
139 if cfg_verity == booted_verity {
140 cfg.sort_key = Some(secondary_sort_key(os_id));
142 } else {
143 cfg.sort_key = Some(primary_sort_key(os_id));
145 }
146 }
147
148 boot_dir
150 .create_dir_all(TYPE1_ENT_PATH_STAGED)
151 .context("Creating staged dir")?;
152
153 let rollback_entries_dir = boot_dir
154 .open_dir(TYPE1_ENT_PATH_STAGED)
155 .context("Opening staged entries dir")?;
156
157 for cfg in all_configs {
159 let cfg_verity = cfg.get_verity()?;
160 let priority = if cfg_verity == booted_verity {
162 FILENAME_PRIORITY_SECONDARY
163 } else {
164 FILENAME_PRIORITY_PRIMARY
165 };
166
167 let file_name = type1_entry_conf_file_name(os_id, &cfg.version(), priority);
168
169 rollback_entries_dir
170 .atomic_write(&file_name, cfg.to_string())
171 .with_context(|| format!("Writing to {file_name}"))?;
172 }
173
174 let rollback_entries_dir = rollback_entries_dir
175 .reopen_as_ownedfd()
176 .context("Reopening as owned fd")?;
177
178 fsync(rollback_entries_dir).context("fsync")?;
180
181 let dir = boot_dir.open_dir("loader").context("Opening loader dir")?;
183
184 rename_exchange_bls_entries(&dir)
185}
186
187#[context("Rolling back composefs")]
188pub(crate) async fn composefs_rollback(
189 storage: &Storage,
190 booted_cfs: &BootedComposefs,
191) -> Result<()> {
192 let host = get_composefs_status(storage, booted_cfs).await?;
193
194 let new_spec = {
195 let mut new_spec = host.spec.clone();
196 new_spec.boot_order = new_spec.boot_order.swap();
197 new_spec
198 };
199
200 host.spec.verify_transition(&new_spec)?;
202
203 let reverting = new_spec.boot_order == BootOrder::Default;
204 if reverting {
205 println!("notice: Reverting queued rollback state");
206 }
207
208 let rollback_status = host
209 .status
210 .rollback
211 .ok_or_else(|| anyhow!("No rollback available"))?;
212
213 let Some(rollback_entry) = &rollback_status.composefs else {
217 anyhow::bail!("Rollback deployment not a composefs deployment")
218 };
219
220 let boot_dir = storage.require_boot_dir()?;
221
222 match &rollback_entry.bootloader {
223 Bootloader::Grub => match rollback_entry.boot_type {
224 BootType::Bls => {
225 rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?;
226 }
227 BootType::Uki => {
228 rollback_grub_uki_entries(boot_dir)?;
229 }
230 },
231
232 Bootloader::Systemd => {
233 rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?;
235 }
236 }
237
238 if reverting {
239 println!("Next boot: current deployment");
240 } else {
241 println!("Next boot: rollback deployment");
242 }
243
244 Ok(())
245}