bootc_lib/bootc_composefs/
gc.rs

1//! This module handles the case when deleting a deployment fails midway
2//!
3//! There could be the following cases (See ./delete.rs:delete_composefs_deployment):
4//! - We delete the bootloader entry but fail to delete image
5//! - We delete bootloader + image but fail to delete the state/unrefenced objects etc
6
7use anyhow::{Context, Result};
8use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
9use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
10
11use crate::{
12    bootc_composefs::{
13        delete::{delete_image, delete_staged, delete_state_dir, get_image_objects},
14        status::{
15            get_bootloader, get_composefs_status, get_sorted_grub_uki_boot_entries,
16            get_sorted_type1_boot_entries,
17        },
18    },
19    composefs_consts::{STATE_DIR_RELATIVE, USER_CFG},
20    spec::Bootloader,
21    store::{BootedComposefs, Storage},
22};
23
24#[fn_error_context::context("Listing EROFS images")]
25fn list_erofs_images(sysroot: &Dir) -> Result<Vec<String>> {
26    let images_dir = sysroot
27        .open_dir("composefs/images")
28        .context("Opening images dir")?;
29
30    let mut images = vec![];
31
32    for entry in images_dir.entries_utf8()? {
33        let entry = entry?;
34        let name = entry.file_name()?;
35        images.push(name);
36    }
37
38    Ok(images)
39}
40
41/// Get all Type1/Type2 bootloader entries
42///
43/// # Returns
44/// The fsverity of EROFS images corresponding to boot entries
45#[fn_error_context::context("Listing bootloader entries")]
46fn list_bootloader_entries(storage: &Storage) -> Result<Vec<String>> {
47    let bootloader = get_bootloader()?;
48    let boot_dir = storage.require_boot_dir()?;
49
50    let entries = match bootloader {
51        Bootloader::Grub => {
52            // Grub entries are always in boot
53            let grub_dir = boot_dir.open_dir("grub2").context("Opening grub dir")?;
54
55            if grub_dir.exists(USER_CFG) {
56                // Grub UKI
57                let mut s = String::new();
58                let boot_entries = get_sorted_grub_uki_boot_entries(boot_dir, &mut s)?;
59
60                boot_entries
61                    .into_iter()
62                    .map(|entry| entry.get_verity())
63                    .collect::<Result<Vec<_>, _>>()?
64            } else {
65                // Type1 Entry
66                let boot_entries = get_sorted_type1_boot_entries(boot_dir, true)?;
67
68                boot_entries
69                    .into_iter()
70                    .map(|entry| entry.get_verity())
71                    .collect::<Result<Vec<_>, _>>()?
72            }
73        }
74
75        Bootloader::Systemd => {
76            let boot_entries = get_sorted_type1_boot_entries(boot_dir, true)?;
77
78            boot_entries
79                .into_iter()
80                .map(|entry| entry.get_verity())
81                .collect::<Result<Vec<_>, _>>()?
82        }
83    };
84
85    Ok(entries)
86}
87
88#[fn_error_context::context("Listing state directories")]
89fn list_state_dirs(sysroot: &Dir) -> Result<Vec<String>> {
90    let state = sysroot
91        .open_dir(STATE_DIR_RELATIVE)
92        .context("Opening state dir")?;
93
94    let mut dirs = vec![];
95
96    for dir in state.entries_utf8()? {
97        let dir = dir?;
98
99        if dir.file_type()?.is_file() {
100            continue;
101        }
102
103        dirs.push(dir.file_name()?);
104    }
105
106    Ok(dirs)
107}
108
109/// Deletes objects in sysroot/composefs/objects that are not being referenced by any of the
110/// present EROFS images
111///
112/// We do not delete streams though
113#[fn_error_context::context("Garbage collecting objects")]
114// TODO(Johan-Liebert1): This will be moved to composefs-rs
115pub(crate) fn gc_objects(sysroot: &Dir) -> Result<()> {
116    tracing::debug!("Running garbage collection on unreferenced objects");
117
118    // Get all the objects referenced by all available images
119    let obj_refs = get_image_objects(sysroot)?;
120
121    // List all objects in the objects directory
122    let objects_dir = sysroot
123        .open_dir("composefs/objects")
124        .context("Opening objects dir")?;
125
126    for dir_name in 0x0..=0xff {
127        let dir = objects_dir
128            .open_dir_optional(dir_name.to_string())
129            .with_context(|| format!("Opening {dir_name}"))?;
130
131        let Some(dir) = dir else {
132            continue;
133        };
134
135        for entry in dir.entries_utf8()? {
136            let entry = entry?;
137            let filename = entry.file_name()?;
138
139            let id = Sha512HashValue::from_object_dir_and_basename(dir_name, filename.as_bytes())?;
140
141            // If this object is not referenced by any image, delete it
142            if !obj_refs.contains(&id) {
143                tracing::trace!("Deleting unreferenced object: {filename}");
144
145                entry
146                    .remove_file()
147                    .with_context(|| format!("Removing object {filename}"))?;
148            }
149        }
150    }
151
152    Ok(())
153}
154
155/// 1. List all bootloader entries
156/// 2. List all EROFS images
157/// 3. List all state directories
158/// 4. List staged depl if any
159///
160/// If bootloader entry B1 doesn't exist, but EROFS image B1 does exist, then delete the image and
161/// perform GC
162///
163/// Similarly if EROFS image B1 doesn't exist, but state dir does, then delete the state dir and
164/// perform GC
165#[fn_error_context::context("Running composefs garbage collection")]
166pub(crate) async fn composefs_gc(storage: &Storage, booted_cfs: &BootedComposefs) -> Result<()> {
167    let host = get_composefs_status(storage, booted_cfs).await?;
168    let booted_cfs_status = host.require_composefs_booted()?;
169
170    let sysroot = &storage.physical_root;
171
172    let bootloader_entries = list_bootloader_entries(&storage)?;
173    let images = list_erofs_images(&sysroot)?;
174
175    // Collect the deployments that have an image but no bootloader entry
176    let img_bootloader_diff = images
177        .iter()
178        .filter(|i| !bootloader_entries.contains(i))
179        .collect::<Vec<_>>();
180
181    let staged = &host.status.staged;
182
183    if img_bootloader_diff.contains(&&booted_cfs_status.verity) {
184        anyhow::bail!(
185            "Inconsistent state. Booted entry '{}' found for cleanup",
186            booted_cfs_status.verity
187        )
188    }
189
190    for verity in &img_bootloader_diff {
191        tracing::debug!("Cleaning up orphaned image: {verity}");
192
193        delete_staged(staged)?;
194        delete_image(&sysroot, verity)?;
195        delete_state_dir(&sysroot, verity)?;
196    }
197
198    let state_dirs = list_state_dirs(&sysroot)?;
199
200    // Collect all the deployments that have no image but have a state dir
201    // This for the case where the gc was interrupted after deleting the image
202    let state_img_diff = state_dirs
203        .iter()
204        .filter(|s| !images.contains(s))
205        .collect::<Vec<_>>();
206
207    for verity in &state_img_diff {
208        delete_staged(staged)?;
209        delete_state_dir(&sysroot, verity)?;
210    }
211
212    // Run garbage collection on objects after deleting images
213    gc_objects(&sysroot)?;
214
215    Ok(())
216}