Add Gitea backup restore drill

This commit is contained in:
juvdiaz 2026-05-26 23:07:35 -06:00
parent 8f3ec624c2
commit 047aee8481
2 changed files with 137 additions and 1 deletions

View File

@ -318,18 +318,32 @@ the Gitea pod, copies the dump out of Kubernetes, and stores it under
`/var/backups/homelab/gitea` on the Debian server. The default retention is 30 `/var/backups/homelab/gitea` on the Debian server. The default retention is 30
days. days.
The same install step also creates `homelab-gitea-restore-drill.timer`. The
monthly drill is non-destructive: it verifies the latest backup ZIP, extracts it
to a temporary directory, records a report under
`/var/backups/homelab/gitea-restore-drills`, and removes the temporary extract.
It does not write into the live Gitea PVC.
Run a manual backup from the Debian server with: Run a manual backup from the Debian server with:
```bash ```bash
./lab.sh backup-gitea ./lab.sh backup-gitea
``` ```
Run the restore drill manually with:
```bash
./lab.sh drill-gitea-restore
```
Useful checks: Useful checks:
```bash ```bash
systemctl list-timers homelab-gitea-backup.timer systemctl list-timers homelab-gitea-backup.timer
systemctl list-timers homelab-gitea-restore-drill.timer
sudo systemctl start homelab-gitea-backup.service sudo systemctl start homelab-gitea-backup.service
sudo ls -lh /var/backups/homelab/gitea sudo ls -lh /var/backups/homelab/gitea
sudo ls -lh /var/backups/homelab/gitea-restore-drills
``` ```
## Gitea Actions ## Gitea Actions

124
lab.sh
View File

@ -1178,6 +1178,7 @@ apply_gitea_bootstrap_manifests() {
install_gitea_backup_timer() { install_gitea_backup_timer() {
local backup_script="/usr/local/sbin/homelab-gitea-backup.sh" local backup_script="/usr/local/sbin/homelab-gitea-backup.sh"
local restore_drill_script="/usr/local/sbin/homelab-gitea-restore-drill.sh"
sudo tee "${backup_script}" >/dev/null <<BACKUP_SCRIPT_EOT sudo tee "${backup_script}" >/dev/null <<BACKUP_SCRIPT_EOT
#!/usr/bin/env bash #!/usr/bin/env bash
@ -1261,8 +1262,119 @@ Persistent=true
WantedBy=timers.target WantedBy=timers.target
TIMER_EOT TIMER_EOT
sudo tee "${restore_drill_script}" >/dev/null <<'RESTORE_DRILL_SCRIPT_EOT'
#!/usr/bin/env bash
set -euo pipefail
GITEA_BACKUP_DIR="${GITEA_BACKUP_DIR:-/var/backups/homelab/gitea}"
GITEA_RESTORE_DRILL_DIR="${GITEA_RESTORE_DRILL_DIR:-/var/backups/homelab/gitea-restore-drills}"
GITEA_RESTORE_DRILL_RETENTION_DAYS="${GITEA_RESTORE_DRILL_RETENTION_DAYS:-90}"
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 is required for Gitea restore drills." >&2
exit 1
fi
latest_archive="$(
{ find "${GITEA_BACKUP_DIR}" -maxdepth 1 -type f -name 'gitea-*.zip' -printf '%T@ %p\n' 2>/dev/null || true; } |
sort -nr |
awk 'NR == 1 { sub(/^[^ ]+ /, ""); print }'
)"
if [[ -z "${latest_archive}" ]]; then
echo "Skipping Gitea restore drill: no backup archive found in ${GITEA_BACKUP_DIR}."
exit 0
fi
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
tmp_dir="$(mktemp -d "/tmp/gitea-restore-drill-${timestamp}.XXXXXX")"
tmp_report="$(mktemp "/tmp/gitea-restore-drill-${timestamp}.XXXXXX.txt")"
report_path="${GITEA_RESTORE_DRILL_DIR}/gitea-restore-drill-${timestamp}.txt"
cleanup() {
rm -rf "${tmp_dir}"
rm -f "${tmp_report}"
}
trap cleanup EXIT
python3 - "${latest_archive}" "${tmp_dir}" "${tmp_report}" <<'PY'
import os
import sys
import zipfile
archive_path, extract_dir, report_path = sys.argv[1:4]
with zipfile.ZipFile(archive_path) as archive:
bad_member = archive.testzip()
if bad_member:
raise SystemExit(f"ZIP integrity check failed at {bad_member}")
members = archive.infolist()
if not members:
raise SystemExit("ZIP archive is empty")
extract_root = os.path.abspath(extract_dir)
for member in members:
target = os.path.abspath(os.path.join(extract_root, member.filename))
if target != extract_root and not target.startswith(extract_root + os.sep):
raise SystemExit(f"Unsafe archive path: {member.filename}")
archive.extractall(extract_root)
file_count = 0
total_bytes = 0
for root, _, files in os.walk(extract_dir):
for name in files:
file_count += 1
total_bytes += os.path.getsize(os.path.join(root, name))
if file_count == 0:
raise SystemExit("Archive extracted no files")
with open(report_path, "w", encoding="utf-8") as handle:
handle.write("Gitea restore drill report\n")
handle.write(f"archive={archive_path}\n")
handle.write(f"archive_size_bytes={os.path.getsize(archive_path)}\n")
handle.write(f"extracted_files={file_count}\n")
handle.write(f"extracted_bytes={total_bytes}\n")
handle.write("result=ok\n")
PY
sudo mkdir -p "${GITEA_RESTORE_DRILL_DIR}"
sudo install -m 0640 -o root -g root "${tmp_report}" "${report_path}"
sudo find "${GITEA_RESTORE_DRILL_DIR}" -type f -name 'gitea-restore-drill-*.txt' -mtime +"${GITEA_RESTORE_DRILL_RETENTION_DAYS}" -delete
echo "Created ${report_path}"
RESTORE_DRILL_SCRIPT_EOT
sudo chmod 0755 "${restore_drill_script}"
sudo tee /etc/systemd/system/homelab-gitea-restore-drill.service >/dev/null <<'RESTORE_DRILL_SERVICE_EOT'
[Unit]
Description=Run a non-destructive Gitea backup restore drill
After=network-online.target homelab-gitea-backup.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/homelab-gitea-restore-drill.sh
RESTORE_DRILL_SERVICE_EOT
sudo tee /etc/systemd/system/homelab-gitea-restore-drill.timer >/dev/null <<'RESTORE_DRILL_TIMER_EOT'
[Unit]
Description=Run monthly Homelab Gitea restore drills
[Timer]
OnCalendar=monthly
RandomizedDelaySec=2h
Persistent=true
[Install]
WantedBy=timers.target
RESTORE_DRILL_TIMER_EOT
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl enable --now homelab-gitea-backup.timer >/dev/null sudo systemctl enable --now homelab-gitea-backup.timer >/dev/null
sudo systemctl enable --now homelab-gitea-restore-drill.timer >/dev/null
} }
backup_gitea() { backup_gitea() {
@ -1273,6 +1385,13 @@ backup_gitea() {
sudo /usr/local/sbin/homelab-gitea-backup.sh sudo /usr/local/sbin/homelab-gitea-backup.sh
} }
drill_gitea_restore() {
require_debian_server "drill-gitea-restore"
install_gitea_backup_timer
sudo /usr/local/sbin/homelab-gitea-restore-drill.sh
}
install_gitea_runner() { install_gitea_runner() {
local runner_arch local runner_arch
local runner_home="${GITEA_RUNNER_HOME:-/home/jv/.local/share/gitea-runner/my-homelab-configs}" local runner_home="${GITEA_RUNNER_HOME:-/home/jv/.local/share/gitea-runner/my-homelab-configs}"
@ -1677,6 +1796,9 @@ case "${1:-}" in
backup-gitea) backup-gitea)
backup_gitea backup_gitea
;; ;;
drill-gitea-restore)
drill_gitea_restore
;;
install-gitea-runner) install-gitea-runner)
install_gitea_runner "${2:-}" install_gitea_runner "${2:-}"
;; ;;
@ -1684,7 +1806,7 @@ case "${1:-}" in
nuke nuke
;; ;;
*) *)
echo "Usage: $0 {up|apps|backup-gitea|install-gitea-runner|nuke}" echo "Usage: $0 {up|apps|backup-gitea|drill-gitea-restore|install-gitea-runner|nuke}"
exit 1 exit 1
;; ;;
esac esac