Files
curriculum-project-hub/hub/deploy/backup_silo.sh

90 lines
3.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Create a stopped-instance backup. Business data and recovery credentials are
# deliberately written to different root-owned destinations.
set -euo pipefail
INSTANCE_ID="${INSTANCE_ID:?INSTANCE_ID required}"
ENV_FILE="${ENV_FILE:?ENV_FILE required}"
KEYRING_FILE="${KEYRING_FILE:?KEYRING_FILE required}"
BUSINESS_BACKUP_DIR="${BUSINESS_BACKUP_DIR:?BUSINESS_BACKUP_DIR required}"
RECOVERY_BACKUP_DIR="${RECOVERY_BACKUP_DIR:?RECOVERY_BACKUP_DIR required}"
SERVICE_UNIT="cph-hub-$INSTANCE_ID.service"
SKILL_STORE_ROOT="${SKILL_STORE_ROOT:-/var/lib/cph-hub/$INSTANCE_ID/state/skills}"
[ "$(id -u)" -eq 0 ] || { echo "backup must run as root" >&2; exit 1; }
umask 077
validate_root_secret_file() {
local label="$1" path="$2" metadata
[[ "$path" = /* ]] || { echo "$label must be an absolute path" >&2; exit 1; }
[ ! -L "$path" ] || { echo "$label must not be a symlink: $path" >&2; exit 1; }
[ -f "$path" ] || { echo "$label must be a regular file: $path" >&2; exit 1; }
metadata="$(stat -c '%u:%g:%a' -- "$path")"
[ "$metadata" = "0:0:600" ] || {
echo "$label must be root:root mode 0600; found $metadata" >&2
exit 1
}
}
validate_root_secret_file "ENV_FILE" "$ENV_FILE"
validate_root_secret_file "KEYRING_FILE" "$KEYRING_FILE"
state="$(systemctl show --property=ActiveState --value "$SERVICE_UNIT")"
case "$state" in inactive|failed) ;; *) echo "$SERVICE_UNIT must be stopped; state=$state" >&2; exit 1;; esac
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
: "${DATABASE_URL:?DATABASE_URL missing from ENV_FILE}"
: "${HUB_PROJECT_WORKSPACE_ROOT:?HUB_PROJECT_WORKSPACE_ROOT missing from ENV_FILE}"
[ -d "$HUB_PROJECT_WORKSPACE_ROOT" ] || { echo "workspace root missing" >&2; exit 1; }
[ -d "$SKILL_STORE_ROOT" ] || { echo "skill store root missing" >&2; exit 1; }
install -d -o root -g root -m 0700 "$BUSINESS_BACKUP_DIR" "$RECOVERY_BACKUP_DIR"
business_root="$(realpath -m "$BUSINESS_BACKUP_DIR")"
recovery_root="$(realpath -m "$RECOVERY_BACKUP_DIR")"
workspace_root="$(realpath -m "$HUB_PROJECT_WORKSPACE_ROOT")"
secret_root="$(realpath -m "$(dirname "$KEYRING_FILE")")"
skill_root="$(realpath -m "$SKILL_STORE_ROOT")"
paths_overlap() {
local left="$1" right="$2"
[ "$left" = "$right" ] || [[ "$left/" == "$right/"* ]] || [[ "$right/" == "$left/"* ]]
}
for pair in \
"$business_root|$recovery_root" \
"$business_root|$workspace_root" \
"$recovery_root|$workspace_root" \
"$recovery_root|$secret_root"; do
left="${pair%%|*}"; right="${pair#*|}"
if paths_overlap "$left" "$right"; then
echo "backup destinations must not overlap each other or live state: $left <> $right" >&2
exit 1
fi
done
if paths_overlap "$business_root" "$skill_root" || paths_overlap "$recovery_root" "$skill_root" || paths_overlap "$workspace_root" "$skill_root"; then
echo "backup destinations, workspace and skill store must not overlap: $skill_root" >&2
exit 1
fi
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
business="$business_root/$INSTANCE_ID-$stamp"
recovery="$recovery_root/$INSTANCE_ID-$stamp"
install -d -o root -g root -m 0700 "$business" "$recovery"
pg_dump --format=custom --file="$business/database.dump" "$DATABASE_URL"
tar --create --file="$business/workspaces.tar" --directory="$HUB_PROJECT_WORKSPACE_ROOT" .
tar --create --file="$business/agent-skills.tar" --directory="$SKILL_STORE_ROOT" .
cp --preserve=mode,ownership,timestamps "$KEYRING_FILE" "$recovery/secret-keyring.json"
cp --preserve=mode,ownership,timestamps "$ENV_FILE" "$recovery/platform.env"
chmod 0600 "$recovery/secret-keyring.json" "$recovery/platform.env"
(
cd "$business"
sha256sum database.dump workspaces.tar agent-skills.tar > SHA256SUMS
)
(
cd "$recovery"
sha256sum secret-keyring.json platform.env > SHA256SUMS
)
echo "business backup: $business"
echo "separate recovery backup: $recovery"