bootc_lib/bootc_composefs/
rollback.rs

1use 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/// Atomically rename exchange grub user.cfg with the staged version
26/// Performed as the last step in rollback/update/switch operation
27#[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/// Atomically rename exchange "entries" <-> "entries.staged"
53/// Performed as the last step in rollback/update/switch operation
54///
55/// `entries_dir` is the directory that contains the BLS entries directories
56/// Ex: entries_dir = ESP/loader or boot/loader
57#[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    // TODO(Johan-Liebert): Currently assuming there are only two deployments
91    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/// Performs rollback for
114/// - Grub Type1 boot entries
115/// - Systemd Typ1 boot entries
116/// - Systemd UKI (Type2) boot entries [since we use BLS entries for systemd boot]
117#[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    // Get all boot entries sorted in descending order by sort-key
122    let mut all_configs = get_sorted_type1_boot_entries(&boot_dir, false)?;
123
124    // TODO(Johan-Liebert): Currently assuming there are only two deployments
125    assert!(all_configs.len() == 2);
126
127    // Identify which entry is the currently booted one
128    let booted_bls = get_booted_bls(&boot_dir)?;
129    let booted_verity = booted_bls.get_verity()?;
130
131    // For rollback: previous gets primary sort-key, booted gets secondary sort-key
132    // Use "bootc" as default os_id for rollback scenarios
133    // TODO: Extract actual os_id from deployment
134    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            // This is the currently booted deployment - it should become secondary
141            cfg.sort_key = Some(secondary_sort_key(os_id));
142        } else {
143            // This is the previous deployment - it should become primary (rollback target)
144            cfg.sort_key = Some(primary_sort_key(os_id));
145        }
146    }
147
148    // Write these
149    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    // Write the BLS configs in there
158    for cfg in all_configs {
159        let cfg_verity = cfg.get_verity()?;
160        // After rollback: previous deployment becomes primary, booted becomes secondary
161        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    // Should we sync after every write?
179    fsync(rollback_entries_dir).context("fsync")?;
180
181    // Atomically exchange "entries" <-> "entries.rollback"
182    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    // Just to be sure
201    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    // TODO: Handle staged deployment
214    // Ostree will drop any staged deployment on rollback but will keep it if it is the first item
215    // in the new deployment list
216    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            // We use BLS entries for systemd UKI as well
234            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}