testsAndMisc/linux_configuration/fixes/lib/hosts_guard_migrate.sh
Krzysztof kuhy Rudnicki 4c426eae0e
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Python tests / test (push) Has been cancelled
Make install_core_system.sh work on a fresh machine
Two of the seven modules invoked python_pkg/ paths that stopped existing when
screen_locker and steam_backlog_enforcer were extracted into their own repos,
so the first thing the installer did on a fresh machine was fail. Nothing
installed guard-lib either, which meant the CORE "Midnight shutdown timer"
module always died with "guardctl not found on PATH".

Every module now resolves through install_module_from_repo, and
require_extracted_repo clones a missing sibling instead of only printing
instructions -- otherwise "clone it yourself" is not actionable on the bare
machine this script exists to set up. The clone drops privileges to the
invoking human via sudo -u, because an installer that re-execs under sudo
would otherwise leave a root-owned checkout in the user's home; a sandbox
cannot catch that, since the guest is root throughout. A clone that fails is
still a loud, fatal module failure, never a silent skip.

guard-lib is addressed as the "utils" repo: it is a subdirectory of
github.com/kuhyx/utils, so asking for "guard-lib" would resolve to
~/guard-lib and derive a clone URL that does not exist. It installs first,
before the modules that need guardctl.

Separately, the security hardening suite's screen_locker section hardcoded
$HOME/testsAndMisc/python_pkg/screen_locker/screen_lock.py. That path went
away with the extraction, so all five checks silently SKIPPED -- reporting a
clean run for tests that never executed, which is the exact failure mode
lib/extracted_repos.sh was written to prevent. It resolves the repo properly
now ($HOME is also wrong under sudo), and the assertions are rewritten against
the current implementation: the four symbols they grepped for no longer exist
anywhere, having been replaced by SPORT_CHOICES, SPORT_TABLE_TENNIS,
_validate_table_tennis and MANUAL_WORKOUT_REFLECTION_MIN_CHARS.

On this host that takes the suite from 18 passed / 1 failed / 5 skipped to
23 passed / 0 skipped. The task file's "18 passed / 6 skipped out of 17"
target was stale -- the suite is 24 checks now -- so it could not be matched
literally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-24 12:45:21 +02:00

170 lines
5.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# The migration and rollback steps for hosts-guard, plus their validation and
# status reporting.
#
# Sourced by the entry script, which owns the CLI and the instance registry.
# ----------------------------------------------------------------------------
# Preconditions
# ----------------------------------------------------------------------------
validate_requirements() {
# SYSTEMD_UNIT_DIR and PACMAN_DB_LCK default to the real locations and
# are overridden only by the test harness, which runs un-jailed in
# ci_mirror.sh and in CI where a bind mount would protect nothing.
[[ -x $GUARDCTL ]] || {
err "guardctl not found at $GUARDCTL - run guard-lib's install.sh first"
exit 1
}
local unit
for unit in guard-file@.path guard-file@.service guard-bind-mount@.service; do
[[ -f "${SYSTEMD_UNIT_DIR:-/etc/systemd/system}/$unit" ]] || {
err "missing systemd template ${SYSTEMD_UNIT_DIR:-/etc/systemd/system}/$unit - run guard-lib's install.sh first"
exit 1
}
done
[[ -d $PLUGIN_SRC_DIR ]] || {
err "plugin sources not found at $PLUGIN_SRC_DIR"
exit 1
}
# A live transaction would race every chattr and umount below.
if [[ -e "${PACMAN_DB_LCK:-/var/lib/pacman/db.lck}" ]]; then
err "${PACMAN_DB_LCK:-/var/lib/pacman/db.lck} exists - a pacman transaction is in flight"
exit 1
fi
}
# NOTE: the fresh-install half of this migration (installing plugins and
# registering the three file-guard instances) now also lives in hosts-blocker's
# lib/hosts_guard_setup.sh, which install.sh calls, so a fresh machine no longer
# depends on anyone running this script by hand. What stays here is the part a
# fresh machine has no use for: retiring the LEGACY hosts-guard hooks and units,
# plus --rollback/--status. The two implementations must register the same three
# instances; instance_spec below is the shared contract.
install_plugins() {
run mkdir -p "$PLUGIN_INSTALL_DIR"
local src
for src in "$PLUGIN_SRC_DIR"/*.sh; do
[[ -e $src ]] || continue
run install -m 755 "$src" "$PLUGIN_INSTALL_DIR/$(basename "$src")"
done
msg "plugins installed to $PLUGIN_INSTALL_DIR"
}
# ----------------------------------------------------------------------------
# Migration
# ----------------------------------------------------------------------------
stop_legacy_units_for() { # <instance name>
local prefix="$1"
local unit
for unit in "${LEGACY_UNITS[@]}"; do
[[ $unit == "$prefix"-* ]] || continue
systemctl list-unit-files "$unit" &>/dev/null || continue
run systemctl disable --now "$unit" 2>/dev/null || true
done
}
# Unmount every stacked bind layer so chattr can reach the real inode. Mirrors
# the legacy pacman-pre-unlock-hosts.sh, which had to do exactly this.
# Every branch is a full `if` and the function ends in an explicit `return 0`.
# The obvious `((i > 20)) && break` form leaves the loop body's exit status at 1
# whenever the arithmetic is false, and under `set -e` that aborted the whole
# rollback silently — but ONLY when the loop actually ran, so --dry-run and an
# already-unmounted target both looked fine. It cost a live /etc/hosts left with
# neither the immutable flag nor its read-only mount to notice.
collapse_mounts() { # <path>
local target="$1" i=0
while mountpoint -q "$target"; do
if ! run umount -l "$target" 2>/dev/null; then
break
fi
i=$((i + 1))
if ((i > 20)); then
break
fi
# Nothing is really unmounted under --dry-run, so `mountpoint` stays
# true forever; report the intent once instead of 20 identical lines.
if ((DRY_RUN == 1)); then
break
fi
done
return 0
}
migrate_instance() { # <name>
local name="$1"
local spec target bind plugin also_watch
spec="$(instance_spec "$name")"
IFS='|' read -r target bind plugin also_watch <<<"$spec"
if instance_registered "$name"; then
msg "$name already registered - skipping"
return 0
fi
if [[ ! -e $target ]]; then
warn "$name: target $target does not exist - skipping"
return 0
fi
note "migrating $name ($target)"
stop_legacy_units_for "$name"
collapse_mounts "$target"
run chattr -i "$target" 2>/dev/null || true
local -a args=(file-guard install "$name" --target "$target")
[[ $bind == "yes" ]] && args+=(--bind-mount)
[[ -n $plugin ]] && args+=(--plugin "$PLUGIN_INSTALL_DIR/$plugin")
[[ -n $also_watch ]] && args+=(--also-watch "$also_watch")
run "$GUARDCTL" "${args[@]}"
msg "$name migrated"
}
retire_legacy_hooks() {
local hook removed=0
for hook in "${LEGACY_HOOKS[@]}"; do
if [[ -f "$HOOKS_DIR/$hook" ]]; then
run rm -f "$HOOKS_DIR/$hook"
msg "retired $hook"
removed=1
fi
done
((removed == 0)) && msg "legacy pacman hooks already retired"
return 0
}
do_migrate() {
validate_requirements
save_rollback_state
install_plugins
local name
for name in "${INSTANCES[@]}"; do
migrate_instance "$name"
done
# Only retire the legacy hooks once guard-lib actually owns something -
# otherwise a failed migration would leave the files with NO pacman
# unlock hook at all, and the next transaction would fight chattr +i.
local registered=0
for name in "${INSTANCES[@]}"; do
instance_registered "$name" && registered=$((registered + 1))
done
if ((registered == 0)) && ((DRY_RUN == 0)); then
err "no guard-lib instance registered - refusing to retire the legacy hooks"
exit 1
fi
retire_legacy_hooks
printf "\n"
msg "migration complete ($registered instance(s) registered)"
note "verify with: sudo $SCRIPT_NAME --status"
note "undo with: sudo $SCRIPT_NAME --rollback"
}