Skip to content

Commit 55bad42

Browse files
composefs-backend: Deleting deployments
Add a command to delete a composefs native deployment Deleting a deployment would mean, deleting the EROFS image, the bootloader entries for that deployment and deleting any objects in the composefs repository that are only referenced by said deployment. Also refactor some functions and add error contexts in some places Signed-off-by: Pragyan Poudyal <[email protected]>
1 parent 563b7a0 commit 55bad42

File tree

8 files changed

+431
-10
lines changed

8 files changed

+431
-10
lines changed

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const SYSTEMD_LOADER_CONF_PATH: &str = "loader/loader.conf";
6464
/// directory specified by the BLS spec. We do this because we want systemd-boot to only look at
6565
/// our config files and not show the actual UKIs in the bootloader menu
6666
/// This is relative to the ESP
67-
const SYSTEMD_UKI_DIR: &str = "EFI/Linux/bootc";
67+
pub(crate) const SYSTEMD_UKI_DIR: &str = "EFI/Linux/bootc";
6868

6969
pub(crate) enum BootSetupType<'a> {
7070
/// For initial setup, i.e. install to-disk
@@ -189,9 +189,9 @@ fn compute_boot_digest(
189189
/// Given the SHA256 sum of current VMlinuz + Initrd combo, find boot entry with the same SHA256Sum
190190
///
191191
/// # Returns
192-
/// Returns the verity of the deployment that has a boot digest same as the one passed in
192+
/// Returns the verity of all deployments that have a boot digest same as the one passed in
193193
#[context("Checking boot entry duplicates")]
194-
fn find_vmlinuz_initrd_duplicates(digest: &str) -> Result<Option<String>> {
194+
pub(crate) fn find_vmlinuz_initrd_duplicates(digest: &str) -> Result<Option<Vec<String>>> {
195195
let deployments = Dir::open_ambient_dir(STATE_DIR_ABS, ambient_authority());
196196

197197
let deployments = match deployments {
@@ -201,7 +201,7 @@ fn find_vmlinuz_initrd_duplicates(digest: &str) -> Result<Option<String>> {
201201
Err(e) => anyhow::bail!(e),
202202
};
203203

204-
let mut symlink_to: Option<String> = None;
204+
let mut symlink_to: Option<Vec<String>> = None;
205205

206206
for depl in deployments.entries()? {
207207
let depl = depl?;
@@ -221,8 +221,10 @@ fn find_vmlinuz_initrd_duplicates(digest: &str) -> Result<Option<String>> {
221221
match ini.get::<String>(ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST) {
222222
Some(hash) => {
223223
if hash == digest {
224-
symlink_to = Some(depl_file_name.to_string());
225-
break;
224+
match symlink_to {
225+
Some(ref mut prev) => prev.push(depl_file_name.to_string()),
226+
None => symlink_to = Some(vec![depl_file_name.to_string()]),
227+
}
226228
}
227229
}
228230

@@ -469,6 +471,8 @@ pub(crate) fn setup_composefs_bls_boot(
469471

470472
match find_vmlinuz_initrd_duplicates(&boot_digest)? {
471473
Some(symlink_to) => {
474+
let symlink_to = &symlink_to[0];
475+
472476
match bls_config.cfg_type {
473477
BLSConfigType::NonEFI {
474478
ref mut linux,
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
use std::{collections::HashSet, io::Write, path::Path};
2+
3+
use anyhow::{Context, Result};
4+
use bootc_mount::tempmount::TempMount;
5+
use cap_std_ext::{
6+
cap_std::{ambient_authority, fs::Dir},
7+
dirext::CapStdExtDirExt,
8+
};
9+
use composefs::fsverity::{FsVerityHashValue, Sha256HashValue};
10+
use composefs_boot::bootloader::{EFI_ADDON_DIR_EXT, EFI_EXT};
11+
12+
use crate::{
13+
bootc_composefs::{
14+
boot::{
15+
find_vmlinuz_initrd_duplicates, get_efi_uuid_source, get_esp_partition,
16+
get_sysroot_parent_dev, BootType, SYSTEMD_UKI_DIR,
17+
},
18+
repo::open_composefs_repo,
19+
rollback::{composefs_rollback, rename_exchange_user_cfg},
20+
status::{composefs_deployment_status, get_sorted_uki_boot_entries},
21+
},
22+
composefs_consts::{
23+
BOOT_LOADER_ENTRIES, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR,
24+
STATE_DIR_RELATIVE, USER_CFG_STAGED,
25+
},
26+
parsers::bls_config::{parse_bls_config, BLSConfigType},
27+
spec::{Bootloader, DeploymentEntry},
28+
status::Slot,
29+
};
30+
31+
struct ObjectRefs {
32+
other_depl: HashSet<Sha256HashValue>,
33+
depl_to_del: HashSet<Sha256HashValue>,
34+
}
35+
36+
#[fn_error_context::context("Deleting Type1 Entry {}", depl.deployment.verity)]
37+
fn delete_type1_entries(depl: &DeploymentEntry, boot_dir: &Dir) -> Result<()> {
38+
let entries_dir = boot_dir
39+
.open_dir(BOOT_LOADER_ENTRIES)
40+
.context("Opening entries dir")?;
41+
42+
let should_del_kernel = match &depl.deployment.boot_digest {
43+
Some(digest) => find_vmlinuz_initrd_duplicates(&digest)?
44+
.is_some_and(|vec| vec.iter().any(|digest| *digest != depl.deployment.verity)),
45+
None => false,
46+
};
47+
48+
for entry in entries_dir.entries_utf8()? {
49+
let entry = entry?;
50+
let file_name = entry.file_name()?;
51+
52+
if !file_name.ends_with(".conf") {
53+
// We don't put any non .conf file in the entries dir
54+
// This is here just for sanity
55+
tracing::debug!("Found non .conf file '{file_name}' in entires dir");
56+
continue;
57+
}
58+
59+
let cfg = entries_dir
60+
.read_to_string(&file_name)
61+
.with_context(|| format!("Reading {file_name}"))?;
62+
63+
let bls_config = parse_bls_config(&cfg)?;
64+
65+
match &bls_config.cfg_type {
66+
BLSConfigType::EFI { efi } => {
67+
if !efi.as_str().contains(&depl.deployment.verity) {
68+
continue;
69+
}
70+
71+
// Boot dir in case of EFI will be the ESP
72+
delete_uki(&depl.deployment.verity, boot_dir)?;
73+
entry.remove_file().context("Removing .conf file")?;
74+
75+
break;
76+
}
77+
78+
BLSConfigType::NonEFI { options, .. } => {
79+
let options = options
80+
.as_ref()
81+
.ok_or(anyhow::anyhow!("options not found in BLS config file"))?;
82+
83+
if !options.contains(&depl.deployment.verity) {
84+
continue;
85+
}
86+
87+
if should_del_kernel {
88+
delete_kernel_initrd(&bls_config.cfg_type, boot_dir)?;
89+
}
90+
91+
entry.remove_file().context("Removing .conf file")?;
92+
93+
break;
94+
}
95+
96+
BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"),
97+
}
98+
}
99+
100+
Ok(())
101+
}
102+
103+
#[fn_error_context::context("Deleting kernel and initrd")]
104+
fn delete_kernel_initrd(bls_config: &BLSConfigType, boot_dir: &Dir) -> Result<()> {
105+
let BLSConfigType::NonEFI { linux, initrd, .. } = bls_config else {
106+
anyhow::bail!("Found EFI config")
107+
};
108+
109+
// "linux" and "initrd" are relative to the boot_dir in our config files
110+
boot_dir
111+
.remove_file(linux)
112+
.with_context(|| format!("Removing {linux:?}"))?;
113+
114+
for ird in initrd {
115+
boot_dir
116+
.remove_file(ird)
117+
.with_context(|| format!("Removing {ird:?}"))?;
118+
}
119+
120+
// Remove the directory if it's empty
121+
//
122+
// This shouldn't ever error as we'll never have these in root
123+
let dir = linux
124+
.parent()
125+
.ok_or_else(|| anyhow::anyhow!("Bad path for vmlinuz {linux}"))?;
126+
127+
let kernel_parent_dir = boot_dir.open_dir(&dir)?;
128+
129+
if kernel_parent_dir.entries().iter().len() == 0 {
130+
// We don't have anything other than kernel and initrd in this directory for now
131+
// So this directory should *always* be empty, for now at least
132+
kernel_parent_dir.remove_open_dir()?;
133+
};
134+
135+
Ok(())
136+
}
137+
138+
/// Deletes the UKI `uki_id` and any addons specific to it
139+
#[fn_error_context::context("Deleting UKI and UKI addons {uki_id}")]
140+
fn delete_uki(uki_id: &str, esp_mnt: &Dir) -> Result<()> {
141+
let ukis = esp_mnt.open_dir(SYSTEMD_UKI_DIR)?;
142+
143+
for entry in ukis.entries_utf8()? {
144+
let entry = entry?;
145+
let entry_name = entry.file_name()?;
146+
147+
// The actual UKI PE binary
148+
if entry_name == format!("{}{}", uki_id, EFI_EXT) {
149+
entry.remove_file().context("Deleting UKI")?;
150+
} else if entry_name == format!("{}{}", uki_id, EFI_ADDON_DIR_EXT) {
151+
// Addons dir
152+
ukis.remove_dir_all(entry_name)
153+
.context("Deleting UKI addons dir")?;
154+
}
155+
}
156+
157+
Ok(())
158+
}
159+
160+
#[fn_error_context::context("Removing Grub Menuentry")]
161+
fn remove_grub_menucfg_entry(id: &str, boot_dir: &Dir) -> Result<()> {
162+
let mut string = String::new();
163+
let menuentries = get_sorted_uki_boot_entries(boot_dir, &mut string)?;
164+
165+
let mut buffer = vec![];
166+
167+
buffer.write_all(get_efi_uuid_source().as_bytes())?;
168+
169+
for entry in menuentries {
170+
if !entry.body.chainloader.contains(id) {
171+
continue;
172+
}
173+
174+
buffer.write_all(entry.to_string().as_bytes())?;
175+
}
176+
177+
let grub_dir = boot_dir.open_dir("grub2").context("Opening grub2")?;
178+
179+
grub_dir
180+
.atomic_write(USER_CFG_STAGED, buffer)
181+
.with_context(|| format!("Writing to {USER_CFG_STAGED}"))?;
182+
183+
rustix::fs::fsync(grub_dir.reopen_as_ownedfd().context("Reopening")?).context("fsync")?;
184+
185+
rename_exchange_user_cfg(&grub_dir)
186+
}
187+
188+
fn delete_depl_boot_entries(deployment: &DeploymentEntry, deleting_staged: bool) -> Result<()> {
189+
match deployment.deployment.bootloader {
190+
Bootloader::Grub => {
191+
let boot_dir = Dir::open_ambient_dir("/sysroot/boot", ambient_authority())
192+
.context("Opening boot dir")?;
193+
194+
match deployment.deployment.boot_type {
195+
BootType::Bls => delete_type1_entries(deployment, &boot_dir),
196+
197+
BootType::Uki => {
198+
let device = get_sysroot_parent_dev()?;
199+
let (esp_part, ..) = get_esp_partition(&device)?;
200+
let esp_mount = TempMount::mount_dev(&esp_part)?;
201+
202+
delete_uki(&deployment.deployment.verity, &esp_mount.fd)?;
203+
204+
remove_grub_menucfg_entry(&deployment.deployment.verity, &boot_dir)
205+
}
206+
}
207+
}
208+
209+
Bootloader::Systemd => {
210+
let device = get_sysroot_parent_dev()?;
211+
let (esp_part, ..) = get_esp_partition(&device)?;
212+
213+
let esp_mount = TempMount::mount_dev(&esp_part)?;
214+
215+
// For Systemd UKI as well, we use .conf files
216+
delete_type1_entries(deployment, &esp_mount.fd)
217+
}
218+
}
219+
}
220+
221+
pub(crate) async fn delete_composefs_deployment(deployment_id: &str, delete: bool) -> Result<()> {
222+
let host = composefs_deployment_status().await?;
223+
224+
let booted = host.require_composefs_booted()?;
225+
226+
let all_depls = host.all_composefs_deployments()?;
227+
228+
let depl_to_del = all_depls
229+
.iter()
230+
.find(|d| d.deployment.verity == deployment_id);
231+
232+
let Some(depl_to_del) = depl_to_del else {
233+
anyhow::bail!("Deployment {deployment_id} not found");
234+
};
235+
236+
let deleting_staged = host
237+
.status
238+
.staged
239+
.as_ref()
240+
.and_then(|s| s.composefs.as_ref())
241+
.map_or(false, |cfs| cfs.verity == deployment_id);
242+
243+
// Get all objects referenced by all images
244+
// Delete objects that are only referenced by the deployment to be deleted
245+
246+
// Unqueue rollback. This makes it easier to delete boot entries later on
247+
if matches!(depl_to_del.ty, Some(Slot::Rollback)) && host.status.rollback_queued {
248+
composefs_rollback().await?;
249+
}
250+
251+
let sysroot =
252+
Dir::open_ambient_dir("/sysroot", ambient_authority()).context("Opening sysroot")?;
253+
254+
let repo = open_composefs_repo(&sysroot)?;
255+
256+
let images_dir = sysroot
257+
.open_dir("composefs/images")
258+
.context("Opening images dir")?;
259+
260+
let image_entries = images_dir
261+
.entries_utf8()
262+
.context("Reading entries in images dir")?;
263+
264+
let mut object_refs = ObjectRefs {
265+
other_depl: HashSet::new(),
266+
depl_to_del: HashSet::new(),
267+
};
268+
269+
for image in image_entries {
270+
let image = image?;
271+
272+
let img_name = image.file_name().context("Getting image name")?;
273+
274+
let objects = repo
275+
.objects_for_image(&img_name)
276+
.with_context(|| format!("Getting objects for image {img_name}"))?;
277+
278+
if img_name == deployment_id {
279+
object_refs.depl_to_del.extend(objects);
280+
} else {
281+
object_refs.other_depl.extend(objects);
282+
}
283+
}
284+
285+
let diff: Vec<&Sha256HashValue> = object_refs
286+
.depl_to_del
287+
.difference(&object_refs.other_depl)
288+
.collect();
289+
290+
tracing::debug!("diff: {:#?}", diff);
291+
292+
// For debugging, but maybe useful elsewhere?
293+
if !delete {
294+
return Ok(());
295+
}
296+
297+
if deployment_id == &booted.verity {
298+
anyhow::bail!("Cannot delete currently booted deployment");
299+
}
300+
301+
let kind = if depl_to_del.pinned {
302+
"pinned "
303+
} else if deleting_staged {
304+
"staged "
305+
} else {
306+
""
307+
};
308+
309+
tracing::info!("Deleting {kind}deployment '{deployment_id}'");
310+
311+
for sha in diff {
312+
let object_path = Path::new("composefs")
313+
.join("objects")
314+
.join(sha.to_object_pathname());
315+
316+
sysroot
317+
.remove_file(&object_path)
318+
.with_context(|| format!("Removing {object_path:?}"))?;
319+
}
320+
321+
let state_dir = Path::new(STATE_DIR_RELATIVE).join(deployment_id);
322+
sysroot
323+
.remove_dir_all(&state_dir)
324+
.with_context(|| format!("Removing dir {state_dir:?}"))?;
325+
326+
if deleting_staged {
327+
let file = Path::new(COMPOSEFS_TRANSIENT_STATE_DIR).join(COMPOSEFS_STAGED_DEPLOYMENT_FNAME);
328+
tracing::debug!("Deleting staged file {file:?}");
329+
std::fs::remove_file(file).context("Removing staged file")?;
330+
}
331+
332+
delete_depl_boot_entries(&depl_to_del, deleting_staged)?;
333+
334+
// Delete the image
335+
let img_path = Path::new("composefs").join("images").join(deployment_id);
336+
sysroot
337+
.remove_file(&img_path)
338+
.context("Deleting EROFS image")?;
339+
340+
Ok(())
341+
}

crates/lib/src/bootc_composefs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub(crate) mod boot;
2+
pub(crate) mod delete;
23
pub(crate) mod finalize;
34
pub(crate) mod repo;
45
pub(crate) mod rollback;

0 commit comments

Comments
 (0)