Skip to main content

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