#!/usr/bin/env bash # Install/update without overwriting local configuration or starting a backup. set -Eeuo pipefail umask 077 ROOT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) [[ $EUID == 0 ]] || { echo 'Use sudo ./install.sh [--enable-timer]' >&2; exit 1; } ENABLE_TIMER=false NO_TIMER=false SNAPSHOT_DIR='' while (( $# )); do case $1 in --enable-timer) ENABLE_TIMER=true; shift ;; --no-timer) NO_TIMER=true; shift ;; --snapshot-dir) SNAPSHOT_DIR=${2:?Missing snapshot directory}; shift 2 ;; *) echo 'Usage: sudo ./install.sh [--enable-timer | --no-timer] [--snapshot-dir DIR]' >&2; exit 1 ;; esac done [[ $NO_TIMER != true || $ENABLE_TIMER != true ]] || exit 1 # Syntax and packaging errors must be caught BEFORE changing the running installation. for file in "$ROOT_DIR"/usr-local-sbin/*.sh "$ROOT_DIR"/lib/*.sh; do bash -n "$file"; done python3 - "$ROOT_DIR"/lib/*.py <<'PY' import ast,sys for path in sys.argv[1:]: ast.parse(open(path).read()) PY LOCKFILE=/run/app-backup.lock if [[ -f /etc/app-backup/app-backup.conf ]]; then [[ ! -L /etc/app-backup/app-backup.conf && $(stat -c %u /etc/app-backup/app-backup.conf) == 0 ]] || { echo 'Unsafe config owner/type'; exit 1; } (( (8#$(stat -c %a /etc/app-backup/app-backup.conf) & 8#022) == 0 )) || { echo 'Unsafe config permissions'; exit 1; } # Respect a customized lock path. Config is trusted root-owned Bash. # shellcheck disable=SC1091 source /etc/app-backup/app-backup.conf fi # shellcheck source=lib/locking.sh source "$ROOT_DIR/lib/locking.sh" acquire_backup_lock 1 if systemctl is-active --quiet app-backup.service; then echo 'app-backup.service is active. Wait for completion before updating.' >&2 exit 1 fi if systemctl is-active --quiet app-restore-test.service; then echo 'Restore test is active. Wait for completion before updating.' >&2 exit 1 fi TIMER_ACTIVE=false if systemctl is-active --quiet app-backup.timer; then TIMER_ACTIVE=true; fi RESTORE_TIMER_ACTIVE=false if systemctl is-active --quiet app-restore-test.timer; then RESTORE_TIMER_ACTIVE=true; fi if [[ -n $SNAPSHOT_DIR ]]; then [[ $SNAPSHOT_DIR == /* && ! -e $SNAPSHOT_DIR ]] || { echo 'Snapshot directory must be new and absolute'; exit 1; } mkdir -m 700 -- "$SNAPSHOT_DIR" SNAPSHOT=$SNAPSHOT_DIR else SNAPSHOT=$(mktemp -d /var/backups/app-backup-update-XXXXXXXX); fi chmod 700 "$SNAPSHOT" declare -a SOURCES TARGETS MODES add() { SOURCES+=("$ROOT_DIR/$1"); TARGETS+=("$2"); MODES+=("$3"); } add usr-local-sbin/app-backup.sh /usr/local/sbin/app-backup.sh 750 add usr-local-sbin/app-restore.sh /usr/local/sbin/app-restore.sh 750 add usr-local-sbin/app-restore-test.sh /usr/local/sbin/app-restore-test.sh 750 add lib/restore_test.py /usr/local/lib/app-backup/restore_test.py 640 add lib/restore-test-remote.sh /usr/local/lib/app-backup/restore-test-remote.sh 640 add systemd/app-restore-test.service /etc/systemd/system/app-restore-test.service 644 add systemd/app-restore-test-cleanup.service /etc/systemd/system/app-restore-test-cleanup.service 644 if [[ -e /etc/systemd/system/app-restore-test.timer ]]; then add systemd/app-restore-test.timer /etc/systemd/system/app-restore-test.timer.dist 644 else add systemd/app-restore-test.timer /etc/systemd/system/app-restore-test.timer 644; fi add lib/common.sh /usr/local/lib/app-backup/common.sh 640 add lib/locking.sh /usr/local/lib/app-backup/locking.sh 640 add lib/space.sh /usr/local/lib/app-backup/space.sh 640 add lib/space.py /usr/local/lib/app-backup/space.py 640 add lib/prepare-restore-test.sh /usr/local/lib/app-backup/prepare-restore-test.sh 640 add lib/backup_set.py /usr/local/lib/app-backup/backup_set.py 640 add lib/report.py /usr/local/lib/app-backup/report.py 640 add lib/usage.py /usr/local/lib/app-backup/usage.py 640 add systemd/app-backup.service /etc/systemd/system/app-backup.service 644 # Preserve existing schedules. The new timer template remains available as .dist. if [[ -e /etc/systemd/system/app-backup.timer ]]; then add systemd/app-backup.timer /etc/systemd/system/app-backup.timer.dist 644 else add systemd/app-backup.timer /etc/systemd/system/app-backup.timer 644; fi add logrotate/app-backup /etc/logrotate.d/app-backup 644 if [[ -e /etc/app-backup/app-backup.conf ]]; then add etc/app-backup/app-backup.conf /etc/app-backup/app-backup.conf.dist 600 else add etc/app-backup/app-backup.conf /etc/app-backup/app-backup.conf 600; fi for f in "$ROOT_DIR"/etc/app-backup/*.example; do add "etc/app-backup/$(basename "$f")" "/etc/app-backup/$(basename "$f")" 600; done add README.md /usr/local/share/doc/app-backup/README.md 644 add Dokumentation.md /usr/local/share/doc/app-backup/Dokumentation.md 644 add UPDATE_ANLEITUNG.md /usr/local/share/doc/app-backup/UPDATE_ANLEITUNG.md 644 add RESTORE_TEST.md /usr/local/share/doc/app-backup/RESTORE_TEST.md 644 add SPEICHER.md /usr/local/share/doc/app-backup/SPEICHER.md 644 add REMOTE_READBACK.md /usr/local/share/doc/app-backup/REMOTE_READBACK.md 644 # Save the exact replaced files plus a rollback inventory; do not dereference links. : > "$SNAPSHOT/present.txt"; : > "$SNAPSHOT/absent.txt" for i in "${!TARGETS[@]}"; do target=${TARGETS[i]} [[ -f ${SOURCES[i]} ]] || { echo "Missing packaged file: ${SOURCES[i]}"; exit 1; } if [[ -e $target || -L $target ]]; then cp -a --parents -- "$target" "$SNAPSHOT" echo "$target" >> "$SNAPSHOT/present.txt" else echo "$target" >> "$SNAPSHOT/absent.txt"; fi done # Include config in the protected snapshot even when it is intentionally preserved. if [[ -f /etc/app-backup/app-backup.conf ]]; then cp -a --parents /etc/app-backup/app-backup.conf "$SNAPSHOT"; fi cat > "$SNAPSHOT/rollback.sh" <<'ROLLBACK' #!/usr/bin/env bash set -euo pipefail [[ $EUID == 0 ]] || exit 1 here=$(cd -- "$(dirname -- "$0")" && pwd) LOCKFILE=/run/app-backup.lock if [[ -f /etc/app-backup/app-backup.conf ]]; then source /etc/app-backup/app-backup.conf; fi # shellcheck disable=SC1091 source "$here/locking.sh" acquire_backup_lock 1 if systemctl is-active --quiet app-backup.service; then echo 'Backup service is active'; exit 1; fi systemctl stop app-backup.timer systemctl disable --now app-restore-test.timer app-restore-test-cleanup.service || true while IFS= read -r target; do cp -a --remove-destination -- "$here$target" "$target"; done < "$here/present.txt" while IFS= read -r target; do rm -f -- "$target"; done < "$here/absent.txt" systemctl daemon-reload echo 'Previous package restored; config and timer state require manual review. Timer remains stopped.' ROLLBACK cp -- "$ROOT_DIR/lib/locking.sh" "$SNAPSHOT/locking.sh" chmod 600 "$SNAPSHOT/locking.sh" chmod 700 "$SNAPSHOT/rollback.sh" CHANGING=false on_exit() { local result=$? if (( result != 0 )) && [[ $CHANGING == true ]]; then echo "UPDATE FAILED. Timer remains stopped. Roll back with: sudo $SNAPSHOT/rollback.sh" >&2 fi } trap on_exit EXIT trap 'exit 130' INT trap 'exit 143' TERM systemctl stop app-backup.timer if [[ $RESTORE_TIMER_ACTIVE == true ]]; then systemctl stop app-restore-test.timer; fi CHANGING=true for i in "${!TARGETS[@]}"; do target=${TARGETS[i]} install -d -m 755 -- "$(dirname -- "$target")" [[ $(dirname -- "$target") != /etc/app-backup ]] || chmod 700 /etc/app-backup # Install via a sibling temp file + rename, avoiding partially-written scripts. tmp=$(mktemp "${target}.update-XXXXXX") install -m "${MODES[i]}" -o root -g root -- "${SOURCES[i]}" "$tmp" mv -fT -- "$tmp" "$target" done install -d -m 700 /var/log/app-backup /var/backups/app-backup systemctl daemon-reload # The coordinator prepares its candidate before install; direct installs prepare # an already configured feature under this same lock before restarting timers. if [[ ${APP_BACKUP_INHERIT_LOCK:-0} != 1 && ${ENABLE_RESTORE_TEST:-false} == true ]]; then APP_BACKUP_INHERIT_LOCK=1 bash /usr/local/lib/app-backup/prepare-restore-test.sh /etc/app-backup/app-backup.conf fi # A persistent timer can fire immediately. Release the package lock only after # all files are installed, but before that timer can start the new backup. if [[ ${APP_BACKUP_INHERIT_LOCK:-0} != 1 ]]; then flock -u 9; fi if [[ $NO_TIMER == true ]]; then : elif [[ $ENABLE_TIMER == true ]]; then systemctl enable --now app-backup.timer elif [[ $TIMER_ACTIVE == true ]]; then systemctl start app-backup.timer; fi if [[ $NO_TIMER != true && $RESTORE_TIMER_ACTIVE == true ]]; then systemctl start app-restore-test.timer; fi CHANGING=false echo "Installed. Rollback package: $SNAPSHOT" echo 'Existing config and timer schedule were preserved. Compare *.dist files and review migration documentation.' echo 'Validate: sudo /usr/local/sbin/app-backup.sh --check' echo 'Fresh installations: configure first, test a backup + restore, then enable app-backup.timer.'