aboutsummaryrefslogtreecommitdiff
path: root/custom
diff options
context:
space:
mode:
Diffstat (limited to 'custom')
-rwxr-xr-xcustom/install-archzfs46
-rwxr-xr-xcustom/zfs-snap-prune208
2 files changed, 254 insertions, 0 deletions
diff --git a/custom/install-archzfs b/custom/install-archzfs
index 660cf34..2cec709 100755
--- a/custom/install-archzfs
+++ b/custom/install-archzfs
@@ -1114,6 +1114,11 @@ if zfs snapshot "$DATASET@$SNAPSHOT_NAME"; then
else
echo "Warning: Failed to create snapshot" >&2
fi
+
+# Prune old snapshots (runs quietly, non-blocking)
+if [[ -x /usr/local/bin/zfs-snap-prune ]]; then
+ /usr/local/bin/zfs-snap-prune --quiet &
+fi
EOF
chmod +x /mnt/usr/local/bin/zfs-pre-snapshot
@@ -1121,6 +1126,46 @@ EOF
info "Pacman hook configured."
}
+configure_snapshot_retention() {
+ step "Configuring Snapshot Retention"
+
+ # Copy the prune script
+ cp /usr/local/bin/zfs-snap-prune /mnt/usr/local/bin/zfs-snap-prune
+ chmod +x /mnt/usr/local/bin/zfs-snap-prune
+
+ # Create systemd service for pruning
+ cat > /mnt/etc/systemd/system/zfs-snap-prune.service << 'EOF'
+[Unit]
+Description=Prune old ZFS snapshots
+After=zfs.target
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/bin/zfs-snap-prune --quiet
+EOF
+
+ # Create systemd timer for daily pruning
+ cat > /mnt/etc/systemd/system/zfs-snap-prune.timer << 'EOF'
+[Unit]
+Description=Daily ZFS snapshot pruning
+
+[Timer]
+OnCalendar=daily
+Persistent=true
+RandomizedDelaySec=1h
+
+[Install]
+WantedBy=timers.target
+EOF
+
+ # Enable the timer
+ arch-chroot /mnt systemctl enable zfs-snap-prune.timer
+
+ info "Snapshot retention configured."
+ info "Policy: Keep 20 recent, delete if older than 180 days"
+ info "Genesis snapshot is always preserved."
+}
+
copy_archsetup() {
step "Installing archsetup Launcher"
@@ -1299,6 +1344,7 @@ main() {
configure_grub_zfs_snap
configure_zfs_services
configure_pacman_hook
+ configure_snapshot_retention
copy_archsetup
sync_efi_partitions
create_genesis_snapshot
diff --git a/custom/zfs-snap-prune b/custom/zfs-snap-prune
new file mode 100755
index 0000000..762ff99
--- /dev/null
+++ b/custom/zfs-snap-prune
@@ -0,0 +1,208 @@
+#!/bin/bash
+# zfs-snap-prune - Prune old ZFS snapshots with hybrid retention policy
+#
+# Retention Policy:
+# - Always keep the N most recent snapshots (default: 20)
+# - Delete snapshots beyond N only if older than MAX_AGE (default: 180 days)
+# - Never delete genesis snapshot
+#
+# Usage:
+# zfs-snap-prune [OPTIONS]
+#
+# Options:
+# --dry-run Show what would be deleted without deleting
+# --verbose Show decision for every snapshot
+# --quiet Suppress non-error output
+# --test Use mock data from stdin instead of real ZFS
+# --help Show this help message
+#
+# Environment variables:
+# POOL_NAME - ZFS pool name (default: zroot)
+# ROOT_DATASET - Root dataset path (default: ROOT/default)
+# KEEP_COUNT - Number of recent snapshots to always keep (default: 20)
+# MAX_AGE_DAYS - Delete older snapshots beyond KEEP_COUNT (default: 180)
+# NOW_OVERRIDE - Override current timestamp for testing (epoch seconds)
+
+set -e
+
+# Configuration (can be overridden by environment)
+POOL_NAME="${POOL_NAME:-zroot}"
+ROOT_DATASET="${ROOT_DATASET:-ROOT/default}"
+KEEP_COUNT="${KEEP_COUNT:-20}"
+MAX_AGE_DAYS="${MAX_AGE_DAYS:-180}"
+
+FULL_DATASET="${POOL_NAME}/${ROOT_DATASET}"
+
+# Flags
+DRY_RUN=false
+VERBOSE=false
+QUIET=false
+TEST_MODE=false
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+usage() {
+ sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
+ exit 0
+}
+
+info() {
+ [[ "$QUIET" == "true" ]] && return
+ echo -e "${GREEN}[INFO]${NC} $1"
+}
+
+verbose() {
+ [[ "$VERBOSE" != "true" ]] && return
+ echo -e "${BLUE}[VERBOSE]${NC} $1"
+}
+
+warn() {
+ [[ "$QUIET" == "true" ]] && return
+ echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+error() {
+ echo -e "${RED}[ERROR]${NC} $1" >&2
+ exit 1
+}
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --dry-run)
+ DRY_RUN=true
+ shift
+ ;;
+ --verbose)
+ VERBOSE=true
+ shift
+ ;;
+ --quiet)
+ QUIET=true
+ shift
+ ;;
+ --test)
+ TEST_MODE=true
+ shift
+ ;;
+ --help|-h)
+ usage
+ ;;
+ *)
+ error "Unknown option: $1"
+ ;;
+ esac
+done
+
+# Check if running as root (skip in test mode)
+if [[ "$TEST_MODE" != "true" ]] && [[ $EUID -ne 0 ]]; then
+ error "This script must be run as root"
+fi
+
+# Get current timestamp (can be overridden for testing)
+NOW="${NOW_OVERRIDE:-$(date +%s)}"
+MAX_AGE_SECONDS=$((MAX_AGE_DAYS * 24 * 60 * 60))
+CUTOFF_TIME=$((NOW - MAX_AGE_SECONDS))
+
+info "Pruning snapshots for ${FULL_DATASET}"
+info "Policy: Keep ${KEEP_COUNT} recent, delete if older than ${MAX_AGE_DAYS} days"
+[[ "$DRY_RUN" == "true" ]] && info "DRY RUN - no changes will be made"
+
+# Get snapshots - either from ZFS or stdin (test mode)
+# Expected format: snapshot_name<TAB>creation_date_string
+# Example: zroot/ROOT/default@pre-pacman_2025-01-15 Wed Jan 15 10:30 2025
+if [[ "$TEST_MODE" == "true" ]]; then
+ # Read mock data from stdin
+ SNAPSHOTS=$(cat | tac)
+else
+ # Query real ZFS - sorted by creation (oldest first), then reversed for newest first
+ SNAPSHOTS=$(zfs list -H -t snapshot -o name,creation -s creation -r "$FULL_DATASET" 2>/dev/null | \
+ grep "^${FULL_DATASET}@" | \
+ tac) || true
+fi
+
+if [[ -z "$SNAPSHOTS" ]]; then
+ info "No snapshots found"
+ exit 0
+fi
+
+# Count snapshots
+TOTAL=$(echo "$SNAPSHOTS" | wc -l)
+info "Found ${TOTAL} snapshots"
+
+# Track results
+DELETED=0
+KEPT=0
+POSITION=0
+
+# Process each snapshot
+while IFS=$'\t' read -r snapshot creation_str; do
+ [[ -z "$snapshot" ]] && continue
+
+ POSITION=$((POSITION + 1))
+ SNAP_NAME="${snapshot##*@}"
+
+ # Parse creation time
+ if [[ "$TEST_MODE" == "true" ]]; then
+ # In test mode, creation_str is epoch seconds
+ SNAP_TIME="$creation_str"
+ else
+ # In real mode, parse date string
+ SNAP_TIME=$(date -d "$creation_str" +%s 2>/dev/null || echo "0")
+ fi
+
+ AGE_DAYS=$(( (NOW - SNAP_TIME) / 86400 ))
+
+ # Decision logic
+ if [[ $POSITION -le $KEEP_COUNT ]]; then
+ # Always keep the first KEEP_COUNT snapshots (most recent)
+ verbose "KEEP: ${SNAP_NAME} (position ${POSITION}/${KEEP_COUNT}, ${AGE_DAYS} days old) - within keep count"
+ KEPT=$((KEPT + 1))
+ elif [[ "$SNAP_NAME" == "genesis" ]]; then
+ # Never delete genesis
+ verbose "KEEP: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old) - genesis protected"
+ KEPT=$((KEPT + 1))
+ elif [[ $SNAP_TIME -ge $CUTOFF_TIME ]]; then
+ # Not old enough to delete
+ verbose "KEEP: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old) - younger than ${MAX_AGE_DAYS} days"
+ KEPT=$((KEPT + 1))
+ else
+ # Delete: beyond keep count AND older than max age
+ if [[ "$DRY_RUN" == "true" ]]; then
+ info "WOULD DELETE: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old)"
+ DELETED=$((DELETED + 1))
+ elif [[ "$TEST_MODE" == "true" ]]; then
+ # Test mode: simulate deletion (don't actually call zfs)
+ verbose "DELETE: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old)"
+ DELETED=$((DELETED + 1))
+ else
+ verbose "DELETE: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old)"
+ if zfs destroy "$snapshot" 2>/dev/null; then
+ DELETED=$((DELETED + 1))
+ else
+ warn "Failed to delete ${snapshot}"
+ fi
+ fi
+ fi
+done <<< "$SNAPSHOTS"
+
+# Summary
+info "Summary: ${KEPT} kept, ${DELETED} deleted"
+
+# Regenerate GRUB menu if we deleted anything (skip in dry-run and test modes)
+if [[ $DELETED -gt 0 ]] && [[ "$DRY_RUN" != "true" ]] && [[ "$TEST_MODE" != "true" ]]; then
+ if [[ -x /usr/local/bin/grub-zfs-snap ]]; then
+ info "Regenerating GRUB menu..."
+ /usr/local/bin/grub-zfs-snap
+ fi
+fi
+
+# Exit with special code for testing (number of deleted)
+if [[ "$TEST_MODE" == "true" ]]; then
+ echo "RESULT:kept=${KEPT},deleted=${DELETED}"
+fi