Adds a server-wide backup capability beyond the existing ops_dashboard pg_dump flow: - Daily systemd timer (03:30) runs pg_dumpall + Forgejo dump, then restic to a local NAS repo and an offsite Backblaze B2 repo with Object Lock. Phase-based script with single-instance flock, structured statusfile, systemd hardening, and live-datadir excludes (Postgres / Forgejo) so the dumps stay authoritative. - Ops-agent gets nine new read-only/trigger commands (snapshots, stats, status, logs, plus two triggers) backed by sudoers-whitelisted wrapper scripts that source /etc/restic-backup.env so the agent never sees the restic password or B2 keys. - Two new flows (server_backup_full, server_backup_restore_test) drive the dashboard's "Backup now" and "Restore test" buttons. - /settings/backups gains a Server backup section with overall + per-phase status, NAS / B2 snapshot tables, restore-size / raw-data / dedup-ratio stats, and the last restore-test result. The existing pg_dump section is preserved unchanged. - Runbook docs/runbooks/server-backup.md follows the tailscale-setup pattern (plan + addendum) and covers B2 Object Lock + scoped keys, Forgejo subplan with isolated restore-test stack, the off-server maintenance flow for B2 prune, and the integrity-check schedule. Code-only change — installation on scrum4me-srv follows the runbook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
51 lines
1.7 KiB
Bash
51 lines
1.7 KiB
Bash
#!/usr/bin/env bash
|
|
# Repo stats: combines restic stats in two modes plus snapshot count.
|
|
# Output: JSON object with restore_size_bytes, raw_data_bytes, dedup_ratio.
|
|
# Usage: restic-stats.sh nas|b2
|
|
|
|
set -uo pipefail
|
|
|
|
LABEL="${1:-}"
|
|
if [ "$LABEL" != "nas" ] && [ "$LABEL" != "b2" ]; then
|
|
echo '{"error":"label must be nas or b2"}' >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [ -z "${RESTIC_REPO_NAS:-}" ] && [ -r /etc/restic-backup.env ]; then
|
|
set -a; . /etc/restic-backup.env; set +a
|
|
fi
|
|
|
|
case "$LABEL" in
|
|
nas) REPO="${RESTIC_REPO_NAS:?RESTIC_REPO_NAS not set}" ;;
|
|
b2) REPO="${RESTIC_REPO_B2:?RESTIC_REPO_B2 not set}" ;;
|
|
esac
|
|
|
|
export RESTIC_PASSWORD_FILE="${RESTIC_PASSWORD_FILE:-/etc/restic-backup.password}"
|
|
|
|
# restore-size: total bytes if every file in every snapshot were re-extracted.
|
|
restore_json=$(restic -r "$REPO" stats --mode restore-size --json 2>/dev/null || echo '{}')
|
|
# raw-data: total unique blob bytes after dedup + compression.
|
|
raw_json=$(restic -r "$REPO" stats --mode raw-data --json 2>/dev/null || echo '{}')
|
|
# Snapshot count for the same repo.
|
|
snap_count=$(restic -r "$REPO" snapshots --json 2>/dev/null | jq 'length // 0')
|
|
|
|
jq -n \
|
|
--arg repo "$LABEL" \
|
|
--argjson restore "$restore_json" \
|
|
--argjson raw "$raw_json" \
|
|
--argjson snap_count "${snap_count:-0}" \
|
|
'
|
|
{
|
|
repo: $repo,
|
|
snapshots_count: $snap_count,
|
|
restore_size_bytes: ($restore.total_size // null),
|
|
restore_size_files: ($restore.total_file_count // null),
|
|
raw_data_bytes: ($raw.total_size // null),
|
|
raw_blob_count: ($raw.total_blob_count // null),
|
|
dedup_ratio: (
|
|
if ($restore.total_size != null) and ($raw.total_size != null) and ($raw.total_size > 0)
|
|
then (($restore.total_size | tonumber) / ($raw.total_size | tonumber))
|
|
else null
|
|
end
|
|
)
|
|
}'
|