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::{GCOpts, 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, EFIKey, parse_bls_config},
18 spec::{BootEntry, BootloaderKind, 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 { key } => {
58 let path = match key {
59 EFIKey::Efi(path) | EFIKey::Uki(path) => path,
60 };
61 if !path.as_str().contains(&depl.deployment.verity) {
62 continue;
63 }
64
65 tracing::debug!("Deleting EFI .conf file: {}", file_name);
67 entry.remove_file().context("Removing .conf file")?;
68
69 break;
70 }
71
72 BLSConfigType::NonEFI { options, .. } => {
73 let options = options
74 .as_ref()
75 .ok_or(anyhow::anyhow!("options not found in BLS config file"))?;
76
77 if !options.contains(&depl.deployment.verity) {
78 continue;
79 }
80
81 tracing::debug!("Deleting non-EFI .conf file: {}", file_name);
82 entry.remove_file().context("Removing .conf file")?;
83
84 break;
85 }
86
87 BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"),
88 }
89 }
90
91 if deleting_staged {
92 tracing::debug!(
93 "Deleting staged entries directory: {}",
94 TYPE1_ENT_PATH_STAGED
95 );
96
97 boot_dir
98 .remove_dir_all(TYPE1_ENT_PATH_STAGED)
99 .context("Removing staged entries dir")?;
100 }
101
102 Ok(())
103}
104
105#[fn_error_context::context("Removing Grub Menuentry")]
106fn remove_grub_menucfg_entry(id: &str, boot_dir: &Dir, deleting_staged: bool) -> Result<()> {
107 let grub_dir = boot_dir.open_dir("grub2").context("Opening grub2")?;
108
109 if deleting_staged {
110 tracing::debug!("Deleting staged grub menuentry file: {}", USER_CFG_STAGED);
111 return grub_dir
112 .remove_file(USER_CFG_STAGED)
113 .context("Deleting staged Menuentry");
114 }
115
116 let mut string = String::new();
117 let menuentries = get_sorted_grub_uki_boot_entries(boot_dir, &mut string)?;
118
119 grub_dir
120 .atomic_replace_with(USER_CFG_STAGED, move |f| -> std::io::Result<_> {
121 f.write_all(get_efi_uuid_source().as_bytes())?;
122
123 for entry in menuentries {
124 if entry.body.chainloader.contains(id) {
125 continue;
126 }
127
128 f.write_all(entry.to_string().as_bytes())?;
129 }
130
131 Ok(())
132 })
133 .with_context(|| format!("Writing to {USER_CFG_STAGED}"))?;
134
135 rustix::fs::fsync(grub_dir.reopen_as_ownedfd().context("Reopening")?).context("fsync")?;
136
137 rename_exchange_user_cfg(&grub_dir)
138}
139
140#[fn_error_context::context("Deleting boot entries for deployment {}", deployment.deployment.verity)]
144fn delete_depl_boot_entries(
145 deployment: &DeploymentEntry,
146 storage: &Storage,
147 deleting_staged: bool,
148) -> Result<()> {
149 let boot_dir = storage.require_boot_dir()?;
150
151 match deployment.deployment.bootloader.kind()? {
152 BootloaderKind::GRUBClassic => match deployment.deployment.boot_type {
153 BootType::Bls => delete_type1_conf_file(deployment, boot_dir, deleting_staged),
154 BootType::Uki => {
155 remove_grub_menucfg_entry(&deployment.deployment.verity, boot_dir, deleting_staged)
156 }
157 },
158
159 BootloaderKind::BLSCompatible => {
160 delete_type1_conf_file(deployment, boot_dir, deleting_staged)
162 }
163 }
164}
165
166#[fn_error_context::context("Deleting state directory for deployment {}", deployment_id)]
167pub(crate) fn delete_state_dir(sysroot: &Dir, deployment_id: &str, dry_run: bool) -> Result<()> {
168 let state_dir = Path::new(STATE_DIR_RELATIVE).join(deployment_id);
169 tracing::debug!("Deleting state directory: {:?}", state_dir);
170
171 if dry_run {
172 return Ok(());
173 }
174
175 sysroot
176 .remove_dir_all(&state_dir)
177 .with_context(|| format!("Removing dir {state_dir:?}"))
178}
179
180#[fn_error_context::context("Deleting staged deployment")]
181pub(crate) fn delete_staged(
182 staged: &Option<BootEntry>,
183 cleanup_list: &Vec<&String>,
184 dry_run: bool,
185) -> Result<()> {
186 let Some(staged_depl) = staged else {
187 tracing::debug!("No staged deployment");
188 return Ok(());
189 };
190
191 if !cleanup_list.contains(&&staged_depl.require_composefs()?.verity) {
192 tracing::debug!("Staged deployment not in cleanup list");
193 return Ok(());
194 }
195
196 let file = Path::new(COMPOSEFS_TRANSIENT_STATE_DIR).join(COMPOSEFS_STAGED_DEPLOYMENT_FNAME);
197
198 if !dry_run && file.exists() {
199 tracing::debug!("Deleting staged deployment file: {file:?}");
200 std::fs::remove_file(file).context("Removing staged file")?;
201 }
202
203 Ok(())
204}
205
206#[fn_error_context::context("Deleting composefs deployment {}", deployment_id)]
207pub(crate) async fn delete_composefs_deployment(
208 deployment_id: &str,
209 storage: &Storage,
210 booted_cfs: &BootedComposefs,
211) -> Result<()> {
212 const COMPOSEFS_DELETE_JOURNAL_ID: &str = "2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6";
213
214 tracing::info!(
215 message_id = COMPOSEFS_DELETE_JOURNAL_ID,
216 bootc.operation = "delete",
217 bootc.current_deployment = booted_cfs.cmdline.digest,
218 bootc.target_deployment = deployment_id,
219 "Starting composefs deployment deletion for {}",
220 deployment_id
221 );
222
223 let host = get_composefs_status(storage, booted_cfs).await?;
224
225 let booted = host.require_composefs_booted()?;
226
227 if deployment_id == &booted.verity {
228 anyhow::bail!("Cannot delete currently booted deployment");
229 }
230
231 let all_depls = host.all_composefs_deployments()?;
232
233 let depl_to_del = all_depls
234 .iter()
235 .find(|d| d.deployment.verity == deployment_id);
236
237 let Some(depl_to_del) = depl_to_del else {
238 anyhow::bail!("Deployment {deployment_id} not found");
239 };
240
241 let deleting_staged = host
242 .status
243 .staged
244 .as_ref()
245 .and_then(|s| s.composefs.as_ref())
246 .map_or(false, |cfs| cfs.verity == deployment_id);
247
248 if matches!(depl_to_del.ty, Some(Slot::Rollback)) && host.status.rollback_queued {
250 composefs_rollback(storage, booted_cfs).await?;
251 }
252
253 let kind = if depl_to_del.pinned {
254 "pinned "
255 } else if deleting_staged {
256 "staged "
257 } else {
258 ""
259 };
260
261 tracing::info!("Deleting {kind}deployment '{deployment_id}'");
262
263 delete_depl_boot_entries(&depl_to_del, &storage, deleting_staged)?;
264
265 composefs_gc(
266 storage,
267 booted_cfs,
268 GCOpts {
269 dry_run: false,
270 prune_repo: true,
271 },
272 )
273 .await?;
274
275 Ok(())
276}