From f59157c64db5632754b48f2b0b8e3253295de689 Mon Sep 17 00:00:00 2001 From: MarcoIeni <11428655+MarcoIeni@users.noreply.github.com> Date: Thu, 30 Apr 2026 19:00:13 +0200 Subject: [PATCH] dev-desktops: clean unused checkout safely --- ansible/roles/dev-desktop/tasks/cleanup.yml | 3 +- .../templates/clean-unused-checkouts.sh | 111 +++++++++++++----- 2 files changed, 85 insertions(+), 29 deletions(-) diff --git a/ansible/roles/dev-desktop/tasks/cleanup.yml b/ansible/roles/dev-desktop/tasks/cleanup.yml index 5d77eff5f..dd4939a03 100644 --- a/ansible/roles/dev-desktop/tasks/cleanup.yml +++ b/ansible/roles/dev-desktop/tasks/cleanup.yml @@ -5,7 +5,8 @@ dest: /etc/cron.cleanup_disk_space owner: root group: root - mode: 0744 + # The script needs to be executable by any user + mode: 0755 - name: Set up the cleanup cron job template: diff --git a/ansible/roles/dev-desktop/templates/clean-unused-checkouts.sh b/ansible/roles/dev-desktop/templates/clean-unused-checkouts.sh index 4e7927ca6..d83e2cabc 100644 --- a/ansible/roles/dev-desktop/templates/clean-unused-checkouts.sh +++ b/ansible/roles/dev-desktop/templates/clean-unused-checkouts.sh @@ -21,8 +21,16 @@ dry_run=false # Default to search for checkouts older than 60 days time="60" +# Internal mode used after dropping privileges to a home directory owner. +cleanup_home_path="" + while [[ $# -gt 0 ]]; do case $1 in + --cleanup-home) + cleanup_home_path="${2}" + shift # past argument + shift # past value + ;; --dry-run) dry_run=true shift # past argument @@ -39,36 +47,83 @@ while [[ $# -gt 0 ]]; do esac done -# Find all build or target directories created by users -# -# This command combines (`-o`) two different conditions to find all build and -# target directories that users have created. Within each home directory, we -# recursively look for directories that either have a file named `x.py` and a -# directory named `build`, or a file named `Cargo.toml` and a directory named -# `target`. -all_cache_directories=$(find /home -type d \( -name build -execdir test -f "x.py" \; -o -name target -execdir test -f "Cargo.toml" \; \) -print | sort | uniq) - -# For each checkout, we want to determine if the user has been working on it -# within the `$time` number of days. -unused_cache_directories=$(for directory in $all_cache_directories; do - project=$(dirname "${directory}") - - # Find all directories with files that have been modified less than $time days ago - modified=$(find "${project}" -type f -mtime -"${time}" -printf '%h\n' | xargs -r dirname | sort | uniq) - - # If no files have been modified in the last 90 days, then the project is - # considered old. - if [[ -z "${modified}" ]]; then - echo "${directory}" +cleanup_home() { + local home="${1}" + local all_cache_directories + local unused_cache_directories + + # This command combines (`-o`) two different conditions to find all build and + # target directories that users have created. Within each home directory, we + # recursively look for directories that either have a file named `x.py` and a + # directory named `build`, or a file named `Cargo.toml` and a directory named + # `target`. + all_cache_directories=$(find "${home}" -type d \( -name build -execdir test -f "x.py" \; -o -name target -execdir test -f "Cargo.toml" \; \) -print | sort | uniq) + + # For each checkout, we want to determine if the user has been working on it + # within the `$time` number of days. + unused_cache_directories=$(for directory in $all_cache_directories; do + project=$(dirname "${directory}") + + # Find all directories with files that have been modified less than $time days ago + modified=$(find "${project}" -type f -mtime -"${time}" -printf '%h\n' | xargs -r dirname | sort | uniq) + + # If no files have been modified in the last `$time` days, then the project is + # considered old. + if [[ -z "${modified}" ]]; then + echo "${directory}" + fi + done) + + # Delete the build directories in the unused checkouts + for directory in $unused_cache_directories; do + if [[ "${dry_run}" == true ]]; then + du -sh "${directory}" + else + echo "Deleting ${directory}" + rm -rf "${directory}" + fi + done +} + +if [[ -n "${cleanup_home_path}" ]]; then + # Ensure this mode isn't called with root privileges. + if [[ "$(id -u)" -eq 0 ]]; then + echo "Error: --cleanup-home must not be run as root." >&2 + exit 1 + fi + + # Ensure the caller only cleans their own home. + if [[ "$(realpath "${cleanup_home_path}")" != "$(realpath "${HOME}")" ]]; then + echo "Error: --cleanup-home may only target your own home directory." >&2 + exit 1 fi -done) -# Delete the build directories in the unused checkouts -for directory in $unused_cache_directories; do + cleanup_home "${cleanup_home_path}" + exit 0 +fi + +# The cron job starts as root so it can enumerate /home. +# Iterate over home directories with null delimiters so unusual names are safe. +find /home -mindepth 1 -maxdepth 1 -type d -print0 | while IFS= read -r -d '' home; do + # Get the owner of the home directory. + uid=$(stat -c '%u' "${home}") || continue + + # Skip home directories owned by root. + [[ "${uid}" -eq 0 ]] && continue + + # Prepare args to re-enter this script in single-home cleanup mode, preserving the age cutoff. + args=(--cleanup-home "${home}" -t "${time}") + + # Keep dry-run behavior consistent when the root-level cron wrapper delegates. if [[ "${dry_run}" == true ]]; then - du -sh "${directory}" - else - echo "Deleting ${directory}" - rm -rf "${directory}" + args+=(--dry-run) + fi + + # Execute this script with the `--cleanup-home` flag to clean up a single user's home directory + # using their privileges. + # Dropping privileges prevents deleting paths outside the user's home in case of malicious symlinks. + # `-H` sets the HOME variable to target user's home dir. + if ! sudo -H -u "#${uid}" -- "$0" "${args[@]}"; then + echo "Warning: cleanup failed for ${home}; continuing with remaining homes." >&2 fi done