Merge pull request #6354 from yuters/fix-snapshot-locate-and-timeline-leak
Fix locate index on Btrfs: skip snapshots, index /home, drain leaked timeline snapshots
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
config_script="$ROOT/install/config/locate.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
stock_conf() {
|
||||
cat >"$1" <<'CONF'
|
||||
PRUNE_BIND_MOUNTS = "yes"
|
||||
PRUNEFS = "9p afs autofs cifs fuse nfs nfs4 proc sysfs tmpfs"
|
||||
PRUNENAMES = ".git .hg .svn"
|
||||
PRUNEPATHS = "/afs /media /mnt /net /sfs /tmp /udev /var/cache /var/lib/pacman/local /var/lock /var/run /var/spool /var/tmp"
|
||||
CONF
|
||||
}
|
||||
|
||||
# updatedb dies on a config that defines a variable twice, so hand every
|
||||
# rewritten file to the real parser rather than trusting the greps below.
|
||||
empty_tree="$test_tmp/empty-tree"
|
||||
mkdir -p "$empty_tree"
|
||||
|
||||
assert_conf_parses() {
|
||||
command -v updatedb >/dev/null || return 0
|
||||
|
||||
local errors
|
||||
errors=$(updatedb --config-file "$1" -U "$empty_tree" -o "$test_tmp/plocate.db" 2>&1 >/dev/null | grep -F "$1:" || true)
|
||||
[[ -z $errors ]] || fail "updatedb accepts the rewritten config" "$errors"
|
||||
}
|
||||
|
||||
conf="$test_tmp/updatedb.conf"
|
||||
stock_conf "$conf"
|
||||
|
||||
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||
|
||||
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config indexes Btrfs subvolume mounts like /home"
|
||||
grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate config prunes /.snapshots"
|
||||
assert_conf_parses "$conf"
|
||||
pass "locate config skips Btrfs snapshots and indexes Btrfs subvolumes"
|
||||
|
||||
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||
|
||||
[[ $(grep -o '/\.snapshots' "$conf" | wc -l) -eq 1 ]] || fail "locate config is idempotent"
|
||||
assert_conf_parses "$conf"
|
||||
pass "locate config leaves an already-configured file alone"
|
||||
|
||||
OMARCHY_UPDATEDB_CONF_PATH="$test_tmp/missing.conf" bash -euo pipefail "$config_script" >/dev/null
|
||||
pass "locate config tolerates a missing updatedb.conf"
|
||||
|
||||
# A hand-edited updatedb.conf may drop the settings entirely, or write them
|
||||
# without the spaces around the "=" or the quotes that the stock Arch file uses.
|
||||
conf="$test_tmp/sparse-updatedb.conf"
|
||||
printf '%s\n' 'PRUNENAMES = ".git .hg .svn"' >"$conf"
|
||||
|
||||
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||
|
||||
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config adds a missing PRUNE_BIND_MOUNTS"
|
||||
grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config adds a missing PRUNEPATHS"
|
||||
assert_conf_parses "$conf"
|
||||
pass "locate config adds settings a hand-edited updatedb.conf is missing"
|
||||
|
||||
conf="$test_tmp/unspaced-updatedb.conf"
|
||||
printf '%s\n' 'PRUNE_BIND_MOUNTS="yes"' 'PRUNEPATHS="/tmp /var/tmp"' >"$conf"
|
||||
|
||||
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||
|
||||
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an unspaced PRUNE_BIND_MOUNTS"
|
||||
grep -qFx 'PRUNEPATHS = "/.snapshots /tmp /var/tmp"' "$conf" || fail "locate config prunes /.snapshots in an unspaced PRUNEPATHS"
|
||||
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config keeps a single PRUNEPATHS setting"
|
||||
assert_conf_parses "$conf"
|
||||
pass "locate config handles updatedb.conf written without spaces around ="
|
||||
|
||||
# updatedb allows a comment after a value and indented settings, and defining
|
||||
# either setting twice makes it refuse to run at all.
|
||||
conf="$test_tmp/commented-updatedb.conf"
|
||||
printf '%s\n' ' PRUNE_BIND_MOUNTS = "yes" # subvolumes look like bind mounts' \
|
||||
'PRUNEPATHS = "/tmp" # scratch' >"$conf"
|
||||
|
||||
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||
|
||||
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an indented PRUNE_BIND_MOUNTS"
|
||||
grep -qFx 'PRUNEPATHS = "/.snapshots /tmp"' "$conf" || fail "locate config keeps the paths a commented PRUNEPATHS already prunes"
|
||||
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config replaces a commented PRUNEPATHS instead of adding a second one"
|
||||
assert_conf_parses "$conf"
|
||||
pass "locate config handles indented settings and trailing comments"
|
||||
|
||||
# A hand-edited file may have dropped the quotes updatedb requires, which
|
||||
# leaves it unparseable until something writes the setting out properly.
|
||||
conf="$test_tmp/unquoted-updatedb.conf"
|
||||
printf '%s\n' 'PRUNEPATHS = /tmp' >"$conf"
|
||||
|
||||
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||
|
||||
grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config repairs an unquoted PRUNEPATHS"
|
||||
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config replaces an unquoted PRUNEPATHS instead of adding a second one"
|
||||
assert_conf_parses "$conf"
|
||||
pass "locate config handles updatedb.conf written without quotes"
|
||||
|
||||
# A path that merely ends in /.snapshots is not the root snapshot directory.
|
||||
conf="$test_tmp/nested-snapshots-updatedb.conf"
|
||||
printf '%s\n' 'PRUNEPATHS = "/var/lib/machines/.snapshots"' >"$conf"
|
||||
|
||||
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||
|
||||
grep -qFx 'PRUNEPATHS = "/.snapshots /var/lib/machines/.snapshots"' "$conf" || fail "locate config prunes /.snapshots alongside a path that ends in it"
|
||||
assert_conf_parses "$conf"
|
||||
pass "locate config tells /.snapshots apart from a path that ends in it"
|
||||
|
||||
locate_migration=$(grep -rl 'Configure locate to skip Btrfs snapshots' "$ROOT/migrations" | head -n 1 || true)
|
||||
[[ -n $locate_migration ]] || fail "locate migration exists"
|
||||
|
||||
fake_bin="$test_tmp/bin"
|
||||
mkdir -p "$fake_bin"
|
||||
|
||||
cat >"$fake_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
exec "$@"
|
||||
STUB
|
||||
chmod +x "$fake_bin/sudo"
|
||||
|
||||
cat >"$fake_bin/systemctl" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'systemctl %s\n' "$*" >>"$TEST_LOG"
|
||||
STUB
|
||||
chmod +x "$fake_bin/systemctl"
|
||||
|
||||
conf="$test_tmp/migration-updatedb.conf"
|
||||
stock_conf "$conf"
|
||||
|
||||
TEST_LOG="$test_tmp/calls.log" \
|
||||
PATH="$fake_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
|
||||
bash -euo pipefail "$locate_migration" >/dev/null
|
||||
|
||||
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate migration rewrites updatedb.conf"
|
||||
grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate migration prunes /.snapshots"
|
||||
grep -qFx 'systemctl restart --no-block plocate-updatedb.service' "$test_tmp/calls.log" || fail "locate migration replaces an in-flight run and rebuilds the index without blocking"
|
||||
pass "locate migration fixes existing installs and rebuilds the index"
|
||||
|
||||
: >"$test_tmp/calls.log"
|
||||
|
||||
TEST_LOG="$test_tmp/calls.log" \
|
||||
PATH="$fake_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
|
||||
bash -euo pipefail "$locate_migration" >/dev/null
|
||||
|
||||
[[ ! -s $test_tmp/calls.log ]] || fail "locate migration skips already-configured installs"
|
||||
pass "locate migration is a no-op once updatedb.conf is configured"
|
||||
|
||||
# A dev checkout carries migrations from a release whose install scripts the
|
||||
# checked-out tree may not have yet, and omarchy-migrate runs under set -e.
|
||||
: >"$test_tmp/calls.log"
|
||||
conf="$test_tmp/no-config-script-updatedb.conf"
|
||||
stock_conf "$conf"
|
||||
|
||||
TEST_LOG="$test_tmp/calls.log" \
|
||||
PATH="$fake_bin:$PATH" \
|
||||
OMARCHY_PATH="$test_tmp/empty" \
|
||||
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
|
||||
bash -euo pipefail "$locate_migration" >/dev/null ||
|
||||
fail "locate migration survives a tree without the locate config script"
|
||||
|
||||
[[ ! -s $test_tmp/calls.log ]] || fail "locate migration touches nothing without the locate config script"
|
||||
pass "locate migration is a no-op when the locate config script is missing"
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
leak_migration=$(grep -rl 'timeline snapshots leaked by earlier defaults' "$ROOT/migrations" | head -n 1 || true)
|
||||
[[ -n $leak_migration ]] || fail "Snapper timeline leak migration exists"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
fake_bin="$test_tmp/bin"
|
||||
mkdir -p "$fake_bin"
|
||||
|
||||
cat >"$fake_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'sudo %s\n' "$*" >>"$TEST_LOG"
|
||||
exec "$@"
|
||||
STUB
|
||||
chmod +x "$fake_bin/sudo"
|
||||
|
||||
cat >"$fake_bin/snapper" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'snapper %s\n' "$*" >>"$TEST_LOG"
|
||||
if [[ "$*" == *"--csvout list"* ]]; then
|
||||
echo "number,cleanup"
|
||||
for i in $(seq 1 45); do
|
||||
echo "$i,timeline"
|
||||
done
|
||||
echo "100,number"
|
||||
echo "101,"
|
||||
fi
|
||||
STUB
|
||||
chmod +x "$fake_bin/snapper"
|
||||
|
||||
snapper_config="$test_tmp/root"
|
||||
printf '%s\n' 'TIMELINE_CREATE="no"' 'NUMBER_CLEANUP="yes"' >"$snapper_config"
|
||||
|
||||
TEST_LOG="$test_tmp/calls.log" \
|
||||
PATH="$fake_bin:$PATH" \
|
||||
OMARCHY_SNAPPER_CONFIG_PATH="$snapper_config" \
|
||||
bash -euo pipefail "$leak_migration" >/dev/null
|
||||
|
||||
deletes=$(grep -c '^snapper -c root delete ' "$test_tmp/calls.log" || true)
|
||||
[[ $deletes -eq 3 ]] || fail "leak migration deletes snapshots in batches" "expected 3 delete calls, got $deletes"
|
||||
|
||||
first_batch=$(grep -m1 '^snapper -c root delete ' "$test_tmp/calls.log")
|
||||
[[ $first_batch == "snapper -c root delete $(seq -s ' ' 1 20)" ]] || fail "leak migration caps delete batches at 20 snapshots" "$first_batch"
|
||||
|
||||
last_batch=$(grep '^snapper -c root delete ' "$test_tmp/calls.log" | tail -n 1)
|
||||
[[ $last_batch == "snapper -c root delete $(seq -s ' ' 41 45)" ]] || fail "leak migration deletes the final partial batch" "$last_batch"
|
||||
|
||||
! grep -E '^snapper -c root delete .*\b(100|101)\b' "$test_tmp/calls.log" || fail "leak migration only deletes timeline snapshots"
|
||||
pass "leak migration removes leaked timeline snapshots in batches and keeps the rest"
|
||||
|
||||
# omarchy-migrate runs under set -e, so a batch that dies on a DBus timeout
|
||||
# would otherwise abort the run and skip every migration queued behind it.
|
||||
: >"$test_tmp/calls.log"
|
||||
printf '%s\n' 'TIMELINE_CREATE="no"' 'NUMBER_CLEANUP="yes"' >"$snapper_config"
|
||||
|
||||
cat >"$fake_bin/snapper" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'snapper %s\n' "$*" >>"$TEST_LOG"
|
||||
if [[ "$*" == *"--csvout list"* ]]; then
|
||||
echo "number,cleanup"
|
||||
for i in $(seq 1 45); do
|
||||
echo "$i,timeline"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
echo "failure: dbus timeout" >&2
|
||||
exit 1
|
||||
STUB
|
||||
|
||||
output=$(TEST_LOG="$test_tmp/calls.log" \
|
||||
PATH="$fake_bin:$PATH" \
|
||||
OMARCHY_SNAPPER_CONFIG_PATH="$snapper_config" \
|
||||
bash -euo pipefail "$leak_migration" 2>/dev/null) ||
|
||||
fail "leak migration survives a failed delete batch"
|
||||
|
||||
deletes=$(grep -c '^snapper -c root delete ' "$test_tmp/calls.log" || true)
|
||||
[[ $deletes -eq 3 ]] || fail "leak migration keeps draining after a failed batch" "expected 3 delete calls, got $deletes"
|
||||
|
||||
# omarchy-migrate writes the completion marker even when the drain gave up, so
|
||||
# what is left has to be said out loud rather than left for a rerun.
|
||||
grep -qF '45 snapshots could not be deleted' <<<"$output" || fail "leak migration reports the snapshots it could not delete" "$output"
|
||||
pass "leak migration tolerates a batch that fails partway"
|
||||
|
||||
: >"$test_tmp/calls.log"
|
||||
printf '%s\n' 'TIMELINE_CREATE="yes"' >"$snapper_config"
|
||||
|
||||
TEST_LOG="$test_tmp/calls.log" \
|
||||
PATH="$fake_bin:$PATH" \
|
||||
OMARCHY_SNAPPER_CONFIG_PATH="$snapper_config" \
|
||||
bash -euo pipefail "$leak_migration" >/dev/null
|
||||
|
||||
[[ ! -s $test_tmp/calls.log ]] || fail "leak migration leaves deliberate timeline setups alone"
|
||||
pass "leak migration skips systems where timeline snapshots are intentional"
|
||||
|
||||
: >"$test_tmp/calls.log"
|
||||
|
||||
TEST_LOG="$test_tmp/calls.log" \
|
||||
PATH="$fake_bin:$PATH" \
|
||||
OMARCHY_SNAPPER_CONFIG_PATH="$test_tmp/missing" \
|
||||
bash -euo pipefail "$leak_migration" >/dev/null
|
||||
|
||||
[[ ! -s $test_tmp/calls.log ]] || fail "leak migration skips systems without a Snapper root config"
|
||||
pass "leak migration is a no-op without Snapper configured"
|
||||
|
||||
# Snapper's create-config writes a root-only config, and a config this user
|
||||
# cannot read says nothing about whether timeline snapshots are wanted.
|
||||
: >"$test_tmp/calls.log"
|
||||
printf '%s\n' 'TIMELINE_CREATE="no"' >"$snapper_config"
|
||||
chmod 000 "$snapper_config"
|
||||
|
||||
TEST_LOG="$test_tmp/calls.log" \
|
||||
PATH="$fake_bin:$PATH" \
|
||||
OMARCHY_SNAPPER_CONFIG_PATH="$snapper_config" \
|
||||
bash -euo pipefail "$leak_migration" >/dev/null 2>&1
|
||||
|
||||
chmod 600 "$snapper_config"
|
||||
|
||||
grep -qF "sudo grep -qFx TIMELINE_CREATE=\"no\" $snapper_config" "$test_tmp/calls.log" ||
|
||||
fail "leak migration reads a root-only Snapper config as root" "$(cat "$test_tmp/calls.log")"
|
||||
pass "leak migration does not mistake an unreadable Snapper config for an intentional one"
|
||||
Reference in New Issue
Block a user