Add Gitea backup restore drill
This commit is contained in:
parent
8f3ec624c2
commit
047aee8481
14
README.md
14
README.md
|
|
@ -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
124
lab.sh
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue