bootc_lib/bootc_composefs/
delete.rs

1use std::{io::Write, path::Path};
2
3use anyhow::{Context, Result};
4use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
5
6use crate::{
7    bootc_composefs::{
8        boot::{BootType, get_efi_uuid_source},
9        gc::composefs_gc,
10        rollback::{composefs_rollback, rename_exchange_user_cfg},
11        status::{get_composefs_status, get_sorted_grub_uki_boot_entries},
12    },
13    composefs_consts::{
14        COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, STATE_DIR_RELATIVE,
15        TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED,
16    },
17    parsers::bls_config::{BLSConfigType, parse_bls_config},
18    spec::{BootEntry, Bootloader, DeploymentEntry},
19    status::Slot,
20    store::{BootedComposefs, Storage},
21};
22
23#[fn_error_context::context("Deleting Type1 Entry {}", depl.deployment.verity)]
24fn delete_type1_conf_file(
25    depl: &DeploymentEntry,
26    boot_dir: &Dir,
27    deleting_staged: bool,
28) -> Result<()> {
29    let entries_dir_path = if deleting_staged {
30        TYPE1_ENT_PATH_STAGED
31    } else {
32        TYPE1_ENT_PATH
33    };
34
35    let entries_dir = boot_dir
36        .open_dir(entries_dir_path)
37        .context("Opening entries dir")?;
38
39    for entry in entries_dir.entries_utf8()? {
40        let entry = entry?;
41        let file_name = entry.file_name()?;
42
43        if !file_name.ends_with(".conf") {
44            // We don't put any non .conf file in the entries dir
45            // This is here just for sanity
46            tracing::debug!("Found non .conf file '{file_name}' in entries dir");
47            continue;
48        }
49
50        let cfg = entries_dir
51            .read_to_string(&file_name)
52            .with_context(|| format!("Reading {file_name}"))?;
53
54        let bls_config = parse_bls_config(&cfg)?;
55
56        match &bls_config.cfg_type {
57            BLSConfigType::EFI { efi } => {
58                if !efi.as_str().contains(&depl.deployment.verity) {
59                    continue;
60                }
61
62                // Boot dir in case of EFI will be the ESP
63                tracing::debug!("Deleting EFI .conf file: {}", file_name);
64                entry.remove_file().context("Removing .conf file")?;
65
66                break;
67            }
68
69            BLSConfigType::NonEFI { options, .. } => {
70                let options = options
71                    .as_ref()
72                    .ok_or(anyhow::anyhow!("options not found in BLS config file"))?;
73
74                if !options.contains(&depl.deployment.verity) {
75                    continue;
76                }
77
78                tracing::debug!("Deleting non-EFI .conf file: {}", file_name);
79                entry.remove_file().context("Removing .conf file")?;
80
81                break;
82            }
83
84            BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"),
85        }
86    }
87
88    if deleting_staged {
89        tracing::debug!(
90            "Deleting staged entries directory: {}",
91            TYPE1_ENT_PATH_STAGED
92        );
93
94        boot_dir
95            .remove_dir_all(TYPE1_ENT_PATH_STAGED)
96            .context("Removing staged entries dir")?;
97    }
98
99    Ok(())
100}
101
102#[fn_error_context::context("Removing Grub Menuentry")]
103fn remove_grub_menucfg_entry(id: &str, boot_dir: &Dir, deleting_staged: bool) -> Result<()> {
104    let grub_dir = boot_dir.open_dir("grub2").context("Opening grub2")?;
105
106    if deleting_staged {
107        tracing::debug!("Deleting staged grub menuentry file: {}", USER_CFG_STAGED);
108        return grub_dir
109            .remove_file(USER_CFG_STAGED)
110            .context("Deleting staged Menuentry");
111    }
112
113    let mut string = String::new();
114    let menuentries = get_sorted_grub_uki_boot_entries(boot_dir, &mut string)?;
115
116    grub_dir
117        .atomic_replace_with(USER_CFG_STAGED, move |f| -> std::io::Result<_> {
118            f.write_all(get_efi_uuid_source().as_bytes())?;
119
120            for entry in menuentries {
121                if entry.body.chainloader.contains(id) {
122                    continue;
123                }
124
125                f.write_all(entry.to_string().as_bytes())?;
126            }
127
128            Ok(())
129        })
130        .with_context(|| format!("Writing to {USER_CFG_STAGED}"))?;
131
132    rustix::fs::fsync(grub_dir.reopen_as_ownedfd().context("Reopening")?).context("fsync")?;
133
134    rename_exchange_user_cfg(&grub_dir)
135}
136
137/// Deletes the .conf files in case for systemd-boot and Type1 bootloader entries for Grub
138/// or removes the corresponding menuentry from Grub's user.cfg in case for grub UKI
139/// Does not delete the actual boot binaries
140#[fn_error_context::context("Deleting boot entries for deployment {}", deployment.deployment.verity)]
141fn delete_depl_boot_entries(
142    deployment: &DeploymentEntry,
143    storage: &Storage,
144    deleting_staged: bool,
145) -> Result<()> {
146    let boot_dir = storage.require_boot_dir()?;
147
148    match deployment.deployment.bootloader {
149        Bootloader::Grub => match deployment.deployment.boot_type {
150            BootType::Bls => delete_type1_conf_file(deployment, boot_dir, deleting_staged),
151            BootType::Uki => {
152                remove_grub_menucfg_entry(&deployment.deployment.verity, boot_dir, deleting_staged)
153            }
154        },
155
156        Bootloader::Systemd => {
157            // For Systemd UKI as well, we use .conf files
158            delete_type1_conf_file(deployment, boot_dir, deleting_staged)
159        }
160
161        Bootloader::None => unreachable!("Checked at install time"),
162    }
163}
164
165#[fn_error_context::context("Deleting image for deployment {}", deployment_id)]
166pub(crate) fn delete_image(sysroot: &Dir, deployment_id: &str, dry_run: bool) -> Result<()> {
167    let img_path = Path::new("composefs").join("images").join(deployment_id);
168    tracing::debug!("Deleting EROFS image: {:?}", img_path);
169
170    if dry_run {
171        return Ok(());
172    }
173
174    sysroot
175        .remove_file(&img_path)
176        .context("Deleting EROFS image")
177}
178
179#[fn_error_context::context("Deleting state directory for deployment {}", deployment_id)]
180pub(crate) fn delete_state_dir(sysroot: &Dir, deployment_id: &str, dry_run: bool) -> Result<()> {
181    let state_dir = Path::new(STATE_DIR_RELATIVE).join(deployment_id);
182    tracing::debug!("Deleting state directory: {:?}", state_dir);
183
184    if dry_run {
185        return Ok(());
186    }
187
188    sysroot
189        .remove_dir_all(&state_dir)
190        .with_context(|| format!("Removing dir {state_dir:?}"))
191}
192
193#[fn_error_context::context("Deleting staged deployment")]
194pub(crate) fn delete_staged(
195    staged: &Option<BootEntry>,
196    cleanup_list: &Vec<&String>,
197    dry_run: bool,
198) -> Result<()> {
199    let Some(staged_depl) = staged else {
200        tracing::debug!("No staged deployment");
201        return Ok(());
202    };
203
204    if !cleanup_list.contains(&&staged_depl.require_composefs()?.verity) {
205        tracing::debug!("Staged deployment not in cleanup list");
206        return Ok(());
207    }
208
209    let file = Path::new(COMPOSEFS_TRANSIENT_STATE_DIR).join(COMPOSEFS_STAGED_DEPLOYMENT_FNAME);
210
211    if !dry_run && file.exists() {
212        tracing::debug!("Deleting staged deployment file: {file:?}");
213        std::fs::remove_file(file).context("Removing staged file")?;
214    }
215
216    Ok(())
217}
218
219#[fn_error_context::context("Deleting composefs deployment {}", deployment_id)]
220pub(crate) async fn delete_composefs_deployment(
221    deployment_id: &str,
222    storage: &Storage,
223    booted_cfs: &BootedComposefs,
224) -> Result<()> {
225    const COMPOSEFS_DELETE_JOURNAL_ID: &str = "2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6";
226
227    tracing::info!(
228        message_id = COMPOSEFS_DELETE_JOURNAL_ID,
229        bootc.operation = "delete",
230        bootc.current_deployment = booted_cfs.cmdline.digest,
231        bootc.target_deployment = deployment_id,
232        "Starting composefs deployment deletion for {}",
233        deployment_id
234    );
235
236    let host = get_composefs_status(storage, booted_cfs).await?;
237
238    let booted = host.require_composefs_booted()?;
239
240    if deployment_id == &booted.verity {
241        anyhow::bail!("Cannot delete currently booted deployment");
242    }
243
244    let all_depls = host.all_composefs_deployments()?;
245
246    let depl_to_del = all_depls
247        .iter()
248        .find(|d| d.deployment.verity == deployment_id);
249
250    let Some(depl_to_del) = depl_to_del else {
251        anyhow::bail!("Deployment {deployment_id} not found");
252    };
253
254    let deleting_staged = host
255        .status
256        .staged
257        .as_ref()
258        .and_then(|s| s.composefs.as_ref())
259        .map_or(false, |cfs| cfs.verity == deployment_id);
260
261    // Unqueue rollback. This makes it easier to delete boot entries later on
262    if matches!(depl_to_del.ty, Some(Slot::Rollback)) && host.status.rollback_queued {
263        composefs_rollback(storage, booted_cfs).await?;
264    }
265
266    let kind = if depl_to_del.pinned {
267        "pinned "
268    } else if deleting_staged {
269        "staged "
270    } else {
271        ""
272    };
273
274    tracing::info!("Deleting {kind}deployment '{deployment_id}'");
275
276    delete_depl_boot_entries(&depl_to_del, &storage, deleting_staged)?;
277
278    composefs_gc(storage, booted_cfs, true).await?;
279
280    Ok(())
281}