183 lines
6.0 KiB
Bash
Executable File
183 lines
6.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Verify a Gitea full-server backup dump with a non-production, metadata-only
|
|
# structural drill. This never restores into Gitea, never contacts production,
|
|
# never reads tokens, and never prints archive member names or file contents.
|
|
|
|
set -euo pipefail
|
|
|
|
DUMP_ZIP="${AIOPS_GITEA_FULL_BACKUP_DRILL_DUMP_ZIP:-}"
|
|
HOST_LABEL="${AIOPS_HOST_LABEL:-110}"
|
|
TEXTFILE_PATH="${AIOPS_GITEA_FULL_BACKUP_RESTORE_DRILL_TEXTFILE:-/home/wooo/node_exporter_textfiles/gitea_full_backup_restore_drill.prom}"
|
|
WRITE_TEXTFILE=0
|
|
TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-60}"
|
|
DRILL_SCOPE="structural_metadata_only"
|
|
REQUIRED_COMPONENTS="database repositories app_settings lfs_objects package_registry attachments hooks_custom_assets"
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: scripts/backup/gitea-full-backup-restore-drill.sh [options]
|
|
|
|
Options:
|
|
--dump-zip PATH Gitea dump zip to verify structurally.
|
|
--write-textfile Write node-exporter textfile metric after verification.
|
|
--textfile PATH Textfile metric path.
|
|
--host LABEL Host label for metrics. Default: 110.
|
|
-h, --help Show this help.
|
|
|
|
This verifier inspects only the Gitea dump zip central directory and component
|
|
coverage. It never restores to production, never contacts Gitea, never reads
|
|
credentials, never prints archive member names, and never extracts contents.
|
|
USAGE
|
|
}
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--dump-zip)
|
|
shift
|
|
DUMP_ZIP="${1:?--dump-zip requires PATH}"
|
|
;;
|
|
--write-textfile)
|
|
WRITE_TEXTFILE=1
|
|
;;
|
|
--textfile)
|
|
shift
|
|
TEXTFILE_PATH="${1:?--textfile requires PATH}"
|
|
;;
|
|
--host)
|
|
shift
|
|
HOST_LABEL="${1:?--host requires LABEL}"
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 64
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ -z "$DUMP_ZIP" ]; then
|
|
echo "ERROR=dump_zip_required" >&2
|
|
usage >&2
|
|
exit 64
|
|
fi
|
|
|
|
ok=0
|
|
entry_count=0
|
|
missing_components=""
|
|
error_reason=""
|
|
|
|
if [ ! -f "$DUMP_ZIP" ]; then
|
|
error_reason="dump_zip_not_found"
|
|
else
|
|
set +e
|
|
drill_result="$(
|
|
timeout "$TIMEOUT_SECONDS" python3 - "$DUMP_ZIP" $REQUIRED_COMPONENTS <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
dump_zip = Path(sys.argv[1])
|
|
required = sys.argv[2:]
|
|
|
|
try:
|
|
with zipfile.ZipFile(dump_zip) as archive:
|
|
names = archive.namelist()
|
|
except zipfile.BadZipFile:
|
|
print("ERROR=bad_zip")
|
|
sys.exit(1)
|
|
except OSError:
|
|
print("ERROR=zip_open_failed")
|
|
sys.exit(1)
|
|
|
|
lowered = [name.lower() for name in names]
|
|
|
|
patterns = {
|
|
"database": ("gitea-db", "database", ".sql"),
|
|
"repositories": ("repo", "repos", "repositories", ".git"),
|
|
"app_settings": ("app.ini", "app.example.ini", "conf/"),
|
|
"lfs_objects": ("lfs",),
|
|
"package_registry": ("package", "packages"),
|
|
"attachments": ("attachment", "attachments"),
|
|
"hooks_custom_assets": ("hook", "custom", "public/"),
|
|
}
|
|
|
|
missing = []
|
|
for component in required:
|
|
needles = patterns.get(component, (component,))
|
|
if not any(any(needle in name for needle in needles) for name in lowered):
|
|
missing.append(component)
|
|
|
|
print(f"ENTRY_COUNT={len(names)}")
|
|
print(f"MISSING_COMPONENTS={','.join(missing)}")
|
|
print("OK=1" if not missing and names else "OK=0")
|
|
PY
|
|
)"
|
|
drill_status=$?
|
|
set -e
|
|
entry_count="$(printf '%s\n' "$drill_result" | awk -F= '$1 == "ENTRY_COUNT" {print $2; exit}')"
|
|
missing_components="$(printf '%s\n' "$drill_result" | awk -F= '$1 == "MISSING_COMPONENTS" {print $2; exit}')"
|
|
if [ "$drill_status" -ne 0 ]; then
|
|
error_reason="$(printf '%s\n' "$drill_result" | awk -F= '$1 == "ERROR" {print $2; exit}')"
|
|
[ -n "$error_reason" ] || error_reason="drill_inspection_failed"
|
|
elif printf '%s\n' "$drill_result" | grep -q '^OK=1$'; then
|
|
ok=1
|
|
else
|
|
error_reason="component_coverage_gap"
|
|
fi
|
|
fi
|
|
|
|
timestamp="$(date +%s)"
|
|
dump_label="$(basename "${DUMP_ZIP:-missing}")"
|
|
host_label="${HOST_LABEL//\\/\\\\}"
|
|
host_label="${host_label//\"/\\\"}"
|
|
dump_label="${dump_label//\\/\\\\}"
|
|
dump_label="${dump_label//\"/\\\"}"
|
|
error_label="${error_reason//\\/\\\\}"
|
|
error_label="${error_label//\"/\\\"}"
|
|
|
|
component_present() {
|
|
local component="$1"
|
|
if [ "$ok" -eq 1 ]; then
|
|
printf '1'
|
|
elif printf ',%s,' "$missing_components" | grep -q ",${component},"; then
|
|
printf '0'
|
|
else
|
|
printf '0'
|
|
fi
|
|
}
|
|
|
|
if [ "$WRITE_TEXTFILE" -eq 1 ]; then
|
|
mkdir -p "$(dirname "$TEXTFILE_PATH")"
|
|
tmp_textfile="${TEXTFILE_PATH}.$$"
|
|
{
|
|
echo '# HELP awoooi_gitea_full_backup_restore_drill_performed Whether a non-production Gitea full-backup structural restore drill succeeded.'
|
|
echo '# TYPE awoooi_gitea_full_backup_restore_drill_performed gauge'
|
|
printf 'awoooi_gitea_full_backup_restore_drill_performed{host="%s",service="gitea",drill_scope="%s",production_restore="false",dump="%s",error="%s"} %s\n' "$host_label" "$DRILL_SCOPE" "$dump_label" "$error_label" "$ok"
|
|
printf 'awoooi_gitea_full_backup_restore_drill_timestamp{host="%s",service="gitea",drill_scope="%s"} %s\n' "$host_label" "$DRILL_SCOPE" "$timestamp"
|
|
printf 'awoooi_gitea_full_backup_restore_drill_entry_count{host="%s",service="gitea",drill_scope="%s"} %s\n' "$host_label" "$DRILL_SCOPE" "${entry_count:-0}"
|
|
for component in $REQUIRED_COMPONENTS; do
|
|
printf 'awoooi_gitea_full_backup_restore_drill_component_receipt{host="%s",service="gitea",component="%s",drill_scope="%s"} %s\n' "$host_label" "$component" "$DRILL_SCOPE" "$(component_present "$component")"
|
|
done
|
|
printf 'awoooi_gitea_full_backup_secret_value_collected{host="%s",service="gitea",drill_scope="%s"} 0\n' "$host_label" "$DRILL_SCOPE"
|
|
printf 'awoooi_gitea_full_backup_production_restore_performed{host="%s",service="gitea",drill_scope="%s"} 0\n' "$host_label" "$DRILL_SCOPE"
|
|
} > "$tmp_textfile"
|
|
mv "$tmp_textfile" "$TEXTFILE_PATH"
|
|
fi
|
|
|
|
printf 'GITEA_FULL_BACKUP_RESTORE_DRILL_OK=%s\n' "$ok"
|
|
printf 'ENTRY_COUNT=%s\n' "${entry_count:-0}"
|
|
printf 'TEXTFILE_WRITTEN=%s\n' "$WRITE_TEXTFILE"
|
|
printf 'SECRET_VALUES_COLLECTED=0\n'
|
|
printf 'PRODUCTION_RESTORE_PERFORMED=0\n'
|
|
if [ -n "$error_reason" ]; then
|
|
printf 'ERROR=%s\n' "$error_reason"
|
|
exit 1
|
|
fi
|