| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | instance=${1:?usage: tools/import-dawarich.sh dawarich-preview-XXXXXXXX} |
| 5 | [[ $instance =~ ^dawarich-preview-[0-9a-f]{8}$ ]] || { |
| 6 | echo 'Expected a Dawarich preview ID' >&2 |
| 7 | exit 1 |
| 8 | } |
| 9 | |
| 10 | source_host=${STUDIO_MIGRATION_SOURCE:-zenith} |
| 11 | target_host=${STUDIO_DEPLOY_HOST:-root@127.0.0.1} |
| 12 | target_port=${STUDIO_DEPLOY_PORT:-2222} |
| 13 | remote=(ssh -p "$target_port" "$target_host") |
| 14 | |
| 15 | root=/srv/staging/$instance |
| 16 | source_id=$("${remote[@]}" "python3 -c 'import json; print(json.load(open(\"/var/lib/studio/stages/$instance.json\"))[\"sourceId\"])'") |
| 17 | [[ $source_id == dawarich ]] || { echo 'Preview belongs to another service' >&2; exit 1; } |
| 18 | snapshot=$(ssh "$source_host" '/usr/sbin/zfs list -H -t snapshot -o name -s creation -r storage1/apps | tail -n 1') |
| 19 | [[ $snapshot == storage1/apps@* ]] || { echo 'No Zenith app snapshot is available' >&2; exit 1; } |
| 20 | source_root=/mnt/storage1/apps/.zfs/snapshot/${snapshot#*@}/dawarich_storage |
| 21 | |
| 22 | dataset=$("${remote[@]}" "findmnt -n -o SOURCE --mountpoint $root") |
| 23 | [[ $dataset == */staging/$instance ]] || { echo 'Preview dataset is not mounted' >&2; exit 1; } |
| 24 | |
| 25 | source_secret=$(ssh "$source_host" "sudo -n docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' dawarich-app" | |
| 26 | python3 -c 'import hashlib,sys; values=[line.split("=",1)[1] for line in sys.stdin.read().splitlines() if line.startswith("SECRET_KEY_BASE=")]; assert len(values)==1; print(hashlib.sha256(values[0].encode()).hexdigest())') |
| 27 | own=$("${remote[@]}" "NOMAD_TOKEN=\$(cat /var/lib/studio/nomad.token) nomad var get -out json nomad/jobs/$instance" | |
| 28 | python3 -c 'import hashlib,json,sys; print(hashlib.sha256(json.load(sys.stdin)["Items"]["secret_key_base"].encode()).hexdigest())') |
| 29 | [[ $source_secret == "$own" ]] || { echo 'Dawarich secret differs from Zenith; import the original before restoring data' >&2; exit 1; } |
| 30 | |
| 31 | db_json=$("${remote[@]}" "NOMAD_TOKEN=\$(cat /var/lib/studio/nomad.token) nomad var get -out json nomad/jobs/$instance/inputs/database") |
| 32 | read -r database owner < <(python3 -c 'import json,sys; d=json.load(sys.stdin)["Items"]; print(d["name"], d["username"])' <<<"$db_json") |
| 33 | [[ $database =~ ^dawarich_s_[0-9a-f]{8}$ && $owner == svc_$database ]] || { echo 'Unexpected preview database' >&2; exit 1; } |
| 34 | |
| 35 | allocation=$("${remote[@]}" 'NOMAD_TOKEN=$(cat /var/lib/studio/nomad.token) nomad job allocs -json postgres' | |
| 36 | python3 -c 'import json,sys; ids=[a["ID"] for a in json.load(sys.stdin) if a["ClientStatus"]=="running" and a["DesiredStatus"]=="run"]; assert len(ids)==1; print(ids[0])') |
| 37 | container=app-$allocation |
| 38 | podman='podman --url unix:///run/podman/podman.sock' |
| 39 | scratch=$(mktemp -d) |
| 40 | trap 'rm -rf "$scratch"' EXIT |
| 41 | rsync -a "$source_host:$source_root/" "$scratch/" |
| 42 | drift=$(rsync -rlnc --delete --out-format='%n' "$source_host:$source_root/" "$scratch/") |
| 43 | [[ -z $drift ]] || { echo 'Zenith storage changed during copy; retry the import' >&2; exit 1; } |
| 44 | source_counts=$(ssh "$source_host" 'sudo -n docker exec postgres psql -U postgres -d dawarich -At -c "SELECT (SELECT count(*) FROM points), (SELECT count(*) FROM users)"') |
| 45 | |
| 46 | "${remote[@]}" "NOMAD_TOKEN=\$(cat /var/lib/studio/nomad.token) nomad job stop -yes $instance" |
| 47 | for _ in {1..30}; do |
| 48 | running=$("${remote[@]}" "NOMAD_TOKEN=\$(cat /var/lib/studio/nomad.token) nomad job allocs -json $instance" | |
| 49 | python3 -c 'import json,sys; print(sum(a["ClientStatus"]=="running" for a in json.load(sys.stdin)))') |
| 50 | [[ $running == 0 ]] && break |
| 51 | sleep 2 |
| 52 | done |
| 53 | [[ $running == 0 ]] || { echo 'Dawarich allocation is still running' >&2; exit 1; } |
| 54 | |
| 55 | backup_snapshot=$dataset@before-dawarich-import-$(date +%s)-$$ |
| 56 | "${remote[@]}" "zfs snapshot $backup_snapshot" |
| 57 | rsync -a --delete -e "ssh -p $target_port" "$scratch/" "$target_host:$root/var/app/storage/" |
| 58 | uid=$("${remote[@]}" "python3 -c 'import json; print(json.load(open(\"/var/lib/studio/identities.json\"))[\"dawarich\"])'") |
| 59 | "${remote[@]}" "chown -R $uid:$uid $root/var/app/storage" |
| 60 | drift=$(rsync -rlnc --delete --out-format='%n' -e "ssh -p $target_port" "$scratch/" "$target_host:$root/var/app/storage/") |
| 61 | [[ -z $drift ]] || { echo "Storage copy differs from Zenith; restore $backup_snapshot" >&2; exit 1; } |
| 62 | |
| 63 | backup=/var/lib/studio/$instance-before-import.dump |
| 64 | "${remote[@]}" "umask 077; $podman exec $container pg_dump -U postgres -Fc $database > $backup; test -s $backup" |
| 65 | "${remote[@]}" "$podman exec $container psql -U postgres -d $database -v ON_ERROR_STOP=1 -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public AUTHORIZATION $owner; CREATE EXTENSION postgis;' >/dev/null" |
| 66 | ssh "$source_host" 'sudo -n docker exec postgres pg_dump -U postgres -Fc --no-owner --no-acl --exclude-extension=postgis dawarich' | |
| 67 | "${remote[@]}" "$podman exec -i $container pg_restore -U postgres -d $database --no-owner --no-acl --role=$owner" |
| 68 | target_counts=$("${remote[@]}" "$podman exec $container psql -U postgres -d $database -At -c 'SELECT (SELECT count(*) FROM points), (SELECT count(*) FROM users)'") |
| 69 | [[ $source_counts == "$target_counts" ]] || { echo "Database counts differ from Zenith; restore $backup" >&2; exit 1; } |
| 70 | |
| 71 | python3 "$(dirname "$0")/deploy.py" stage dawarich |
| 72 | "${remote[@]}" "zfs destroy $backup_snapshot" |
| 73 | echo "Imported $instance from $snapshot. Previous database: $backup" |