| 1 | #!/bin/sh |
| 2 | set -eu |
| 3 | |
| 4 | instance=${1:?usage: tools/import-pds.sh pds|pds-preview-XXXXXXXX} |
| 5 | case $instance in |
| 6 | pds) |
| 7 | test -n "${STUDIO_DEPLOY_HOST:-}" || { echo 'Set STUDIO_DEPLOY_HOST to the production target' >&2; exit 1; } |
| 8 | production=true |
| 9 | ;; |
| 10 | pds-preview-*) |
| 11 | printf '%s\n' "$instance" | grep -Eq '^pds-preview-[0-9a-f]{8}$' || { |
| 12 | echo 'Expected a PDS preview ID' >&2; exit 1; |
| 13 | } |
| 14 | production=false |
| 15 | ;; |
| 16 | *) echo 'Expected pds or its preview ID' >&2; exit 1 ;; |
| 17 | esac |
| 18 | |
| 19 | source_host=${STUDIO_MIGRATION_SOURCE:-zenith} |
| 20 | target_host=${STUDIO_DEPLOY_HOST:-root@127.0.0.1} |
| 21 | target_port=${STUDIO_DEPLOY_PORT:-2222} |
| 22 | offline=false |
| 23 | copy_host=$source_host |
| 24 | copy_rsh=ssh |
| 25 | if [ -n "${STUDIO_LEGACY_HANDOFF:-}" ]; then |
| 26 | test "$production" = true || { echo 'Offline handoff requires production PDS' >&2; exit 1; } |
| 27 | sh "$(dirname "$0")/check-legacy-handoff.sh" "$STUDIO_LEGACY_HANDOFF" "$target_host" "$target_port" |
| 28 | offline=true |
| 29 | copy_host=$target_host |
| 30 | copy_rsh="ssh -p $target_port" |
| 31 | fi |
| 32 | source_ssh() { |
| 33 | if [ "$offline" = true ]; then |
| 34 | ssh -p "$target_port" "$target_host" "$@" |
| 35 | else |
| 36 | ssh "$source_host" "$@" |
| 37 | fi |
| 38 | } |
| 39 | if [ "$production" = true ]; then |
| 40 | root=/srv/prod/pds/pds |
| 41 | mount=/srv/prod/pds |
| 42 | if [ "$offline" = false ]; then |
| 43 | source_running=$(ssh "$source_host" "sudo -n docker inspect -f '{{.State.Running}}' pds") |
| 44 | test "$source_running" = false || { echo 'Stop Zenith pds before importing production data' >&2; exit 1; } |
| 45 | fi |
| 46 | response=$(ssh -p "$target_port" "$target_host" 'curl -s -w "\n%{http_code}" -H "X-Nomad-Token: $(cat /var/lib/studio/nomad.token)" http://127.0.0.1:4646/v1/job/pds') |
| 47 | status=$(printf '%s\n' "$response" | tail -n 1) |
| 48 | test "$status" = 200 || { echo "Could not verify pds on the home server: $status" >&2; exit 1; } |
| 49 | stopped=$(printf '%s\n' "$response" | sed '$d' | python3 -c 'import json,sys; print(str(json.load(sys.stdin)["Stop"]).lower())') |
| 50 | test "$stopped" = true || { echo 'Stop pds on the home server before importing production data' >&2; exit 1; } |
| 51 | else |
| 52 | root="/srv/staging/$instance/pds" |
| 53 | mount="/srv/staging/$instance" |
| 54 | source_id=$(ssh -p "$target_port" "$target_host" "python3 -c 'import json; print(json.load(open(\"/var/lib/studio/stages/$instance.json\"))[\"sourceId\"])'") |
| 55 | test "$source_id" = pds || { echo 'Preview belongs to another service' >&2; exit 1; } |
| 56 | fi |
| 57 | scratch=$(mktemp -d) |
| 58 | source_backup= |
| 59 | cleanup() { |
| 60 | rm -rf "$scratch" |
| 61 | if [ -n "$source_backup" ]; then |
| 62 | source_ssh "rm -rf '$source_backup'" |
| 63 | fi |
| 64 | } |
| 65 | trap cleanup EXIT |
| 66 | |
| 67 | dataset=$(ssh -p "$target_port" "$target_host" "findmnt -n -o SOURCE --mountpoint $mount") |
| 68 | if [ "$production" = true ]; then |
| 69 | case $dataset in */prod/pds) ;; *) echo 'Production dataset is not mounted' >&2; exit 1 ;; esac |
| 70 | python3 "$(dirname "$0")/import-pds-secrets.py" |
| 71 | else |
| 72 | case $dataset in */staging/"$instance") ;; *) echo 'Preview dataset is not mounted' >&2; exit 1 ;; esac |
| 73 | origin=$(ssh -p "$target_port" "$target_host" "zfs get -H -o value origin $dataset") |
| 74 | test "$origin" = - || { echo 'Expected a fresh preview dataset' >&2; exit 1; } |
| 75 | fi |
| 76 | snapshot="$dataset@before-pds-import-$(date +%s)-$$" |
| 77 | |
| 78 | source_backup=$(source_ssh python3 - <<'PY' |
| 79 | from pathlib import Path |
| 80 | import shutil |
| 81 | import sqlite3 |
| 82 | import tempfile |
| 83 | |
| 84 | out = Path(tempfile.mkdtemp(prefix="studio-pds-")) |
| 85 | try: |
| 86 | for name in ("account", "sequencer", "did_cache"): |
| 87 | source = sqlite3.connect(f"file:/mnt/storage1/apps/pds/{name}.sqlite?mode=ro", uri=True) |
| 88 | target = sqlite3.connect(out / f"{name}.sqlite") |
| 89 | source.backup(target) |
| 90 | if target.execute("PRAGMA integrity_check").fetchone()[0] != "ok": |
| 91 | raise ValueError(f"Invalid PDS SQLite copy: {name}") |
| 92 | target.close() |
| 93 | source.close() |
| 94 | except BaseException: |
| 95 | shutil.rmtree(out) |
| 96 | raise |
| 97 | print(out) |
| 98 | PY |
| 99 | ) |
| 100 | rsync -a -e "$copy_rsh" "$copy_host:$source_backup/" "$scratch/" |
| 101 | rsync -a --exclude='*.sqlite*' -e "$copy_rsh" "$copy_host:/mnt/storage1/apps/pds/" "$scratch/" |
| 102 | drift=$(rsync -rnc --delete --exclude='*.sqlite*' --out-format='%n' -e "$copy_rsh" "$copy_host:/mnt/storage1/apps/pds/" "$scratch/") |
| 103 | test -z "$drift" || { echo 'PDS source changed during copy; retry the import' >&2; exit 1; } |
| 104 | |
| 105 | if [ "$production" = false ]; then |
| 106 | ssh -p "$target_port" "$target_host" "NOMAD_TOKEN=\$(cat /var/lib/studio/nomad.token) nomad job stop -yes $instance" |
| 107 | fi |
| 108 | ssh -p "$target_port" "$target_host" "set -eu; for i in \$(seq 1 30); do running=\$(curl -fsS -H \"X-Nomad-Token: \$(cat /var/lib/studio/nomad.token)\" http://127.0.0.1:4646/v1/job/$instance/allocations | python3 -c 'import json,sys; print(sum(a[\"ClientStatus\"] == \"running\" for a in json.load(sys.stdin)))'); test \"\$running\" = 0 && break; sleep 2; done; test \"\$running\" = 0; zfs snapshot $snapshot" |
| 109 | |
| 110 | rsync -a --delete -e "ssh -p $target_port" "$scratch/" "$target_host:$root/" |
| 111 | owner=$(ssh -p "$target_port" "$target_host" "python3 -c 'import json; print(json.load(open(\"/var/lib/studio/identities.json\"))[\"pds\"])'") |
| 112 | ssh -p "$target_port" "$target_host" "chown -R $owner:$owner $root; python3 -c 'import sqlite3; from pathlib import Path; root=Path(\"$root\"); names=(\"account\",\"sequencer\",\"did_cache\"); assert all((root / (name + \".sqlite\")).is_file() and sqlite3.connect(\"file:\" + str(root / (name + \".sqlite\")) + \"?mode=ro&immutable=1\", uri=True).execute(\"PRAGMA integrity_check\").fetchone()[0] == \"ok\" for name in names)'" |
| 113 | drift=$(rsync -rnc --delete --out-format='%n' -e "ssh -p $target_port" "$scratch/" "$target_host:$root/") |
| 114 | test -z "$drift" || { echo "PDS copy differs from source backup; restore $snapshot" >&2; exit 1; } |
| 115 | |
| 116 | if [ "$production" = true ]; then |
| 117 | if [ "$offline" = false ]; then |
| 118 | test "$(ssh "$source_host" "sudo -n docker inspect -f '{{.State.Running}}' pds")" = false || { |
| 119 | echo 'Zenith pds restarted during import; leave the home server stopped' >&2; exit 1; |
| 120 | } |
| 121 | fi |
| 122 | echo "Imported production PDS from Zenith. Previous dataset state: $snapshot" |
| 123 | else |
| 124 | python3 "$(dirname "$0")/deploy.py" stage pds |
| 125 | ssh -p "$target_port" "$target_host" "zfs destroy $snapshot" |
| 126 | echo "Imported and tested $instance" |
| 127 | fi |