bootc_lib/bootc_composefs/
delete.rs1use 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 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 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#[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 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 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}