Harden backup integrity, implement verified restore and safe upgrades
This commit is contained in:
1 parent
099688ea48
commit
4d58fc7b66
22 files changed
+1928
-1106
No files matched your search
+112
-41
@@ -1,43 +1,114 @@
|
||||
#!/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
|
||||
case ${1:-} in --enable-timer) ENABLE_TIMER=true ;; '') ;; *) echo 'Usage: sudo ./install.sh [--enable-timer]' >&2; exit 1 ;; esac
|
||||
(( $# <= 1 )) || 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/common.sh"; do bash -n "$file"; done
|
||||
python3 - "$ROOT_DIR/lib/backup_set.py" <<'PY'
|
||||
import ast,sys
|
||||
ast.parse(open(sys.argv[1]).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
|
||||
[[ ! -L $LOCKFILE ]] || exit 1
|
||||
exec 9>"$LOCKFILE"
|
||||
flock -n 9 || { echo 'Backup/restore is running. Retry after it finishes.' >&2; exit 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
|
||||
TIMER_ACTIVE=false
|
||||
if systemctl is-active --quiet app-backup.timer; then TIMER_ACTIVE=true; fi
|
||||
SNAPSHOT=$(mktemp -d /var/backups/app-backup-update-XXXXXXXX)
|
||||
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 lib/common.sh /usr/local/lib/app-backup/common.sh 640
|
||||
add lib/backup_set.py /usr/local/lib/app-backup/backup_set.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
|
||||
# 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
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "-- Installing app-backup..."
|
||||
|
||||
sudo mkdir -p /etc/app-backup
|
||||
sudo mkdir -p /usr/local/sbin
|
||||
sudo mkdir -p /var/log/app-backup
|
||||
sudo mkdir -p /var/backups/app-backup
|
||||
|
||||
# Scripts
|
||||
sudo install -m 0750 -o root -g root "$ROOT_DIR/usr-local-sbin/app-backup.sh" /usr/local/sbin/app-backup.sh
|
||||
sudo install -m 0750 -o root -g root "$ROOT_DIR/usr-local-sbin/app-restore.sh" /usr/local/sbin/app-restore.sh
|
||||
|
||||
# Config (copy once)
|
||||
if [[ ! -f /etc/app-backup/app-backup.conf ]]; then
|
||||
sudo install -m 0640 -o root -g root "$ROOT_DIR/etc/app-backup/app-backup.conf" /etc/app-backup/app-backup.conf
|
||||
echo " - installed /etc/app-backup/app-backup.conf (please adjust paths/db names)"
|
||||
else
|
||||
echo " - /etc/app-backup/app-backup.conf exists, not overwriting"
|
||||
fi
|
||||
|
||||
# DB examples (do not overwrite real files)
|
||||
sudo install -m 0640 -o root -g root "$ROOT_DIR/etc/app-backup/db-wordpress.cnf.example" /etc/app-backup/db-wordpress.cnf.example
|
||||
sudo install -m 0640 -o root -g root "$ROOT_DIR/etc/app-backup/db-nextcloud.cnf.example" /etc/app-backup/db-nextcloud.cnf.example
|
||||
|
||||
# systemd
|
||||
sudo install -m 0644 -o root -g root "$ROOT_DIR/systemd/app-backup.service" /etc/systemd/system/app-backup.service
|
||||
sudo install -m 0644 -o root -g root "$ROOT_DIR/systemd/app-backup.timer" /etc/systemd/system/app-backup.timer
|
||||
|
||||
# logrotate
|
||||
sudo install -m 0644 -o root -g root "$ROOT_DIR/logrotate/app-backup" /etc/logrotate.d/app-backup
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now app-backup.timer
|
||||
|
||||
echo "-- Done."
|
||||
echo "Next steps:"
|
||||
echo "1) Adjust /etc/app-backup/app-backup.conf"
|
||||
echo "2) Create DB cnf files from examples (chmod 600, root:root)"
|
||||
echo "3) Ensure rclone works as root: sudo rclone lsd onedrive:"
|
||||
echo "4) Test run: sudo systemctl start app-backup.service"
|
||||
[[ $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
|
||||
exec 9>"$LOCKFILE"
|
||||
flock -n 9 || { echo 'Backup/restore/update is running'; exit 1; }
|
||||
if systemctl is-active --quiet app-backup.service; then echo 'Backup service is active'; exit 1; fi
|
||||
systemctl stop app-backup.timer
|
||||
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
|
||||
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
|
||||
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
|
||||
# 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.
|
||||
flock -u 9
|
||||
if [[ $ENABLE_TIMER == true ]]; then systemctl enable --now app-backup.timer
|
||||
elif [[ $TIMER_ACTIVE == true ]]; then systemctl start app-backup.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.'
|
||||
Reference in new issue
Block a user