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 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 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 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#[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 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 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}