1#!/bin/sh
2set -eu
3
4instance=${1:?usage: tools/import-pds.sh pds|pds-preview-XXXXXXXX}
5case $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 ;;
17esac
18
19source_host=${STUDIO_MIGRATION_SOURCE:-zenith}
20target_host=${STUDIO_DEPLOY_HOST:-root@127.0.0.1}
21target_port=${STUDIO_DEPLOY_PORT:-2222}
22offline=false
23copy_host=$source_host
24copy_rsh=ssh
25if [ -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"
31fi
32source_ssh() {
33 if [ "$offline" = true ]; then
34 ssh -p "$target_port" "$target_host" "$@"
35 else
36 ssh "$source_host" "$@"
37 fi
38}
39if [ "$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; }
51else
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; }
56fi
57scratch=$(mktemp -d)
58source_backup=
59cleanup() {
60 rm -rf "$scratch"
61 if [ -n "$source_backup" ]; then
62 source_ssh "rm -rf '$source_backup'"
63 fi
64}
65trap cleanup EXIT
66
67dataset=$(ssh -p "$target_port" "$target_host" "findmnt -n -o SOURCE --mountpoint $mount")
68if [ "$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"
71else
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; }
75fi
76snapshot="$dataset@before-pds-import-$(date +%s)-$$"
77
78source_backup=$(source_ssh python3 - <<'PY'
79from pathlib import Path
80import shutil
81import sqlite3
82import tempfile
83
84out = Path(tempfile.mkdtemp(prefix="studio-pds-"))
85try:
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()
94except BaseException:
95 shutil.rmtree(out)
96 raise
97print(out)
98PY
99)
100rsync -a -e "$copy_rsh" "$copy_host:$source_backup/" "$scratch/"
101rsync -a --exclude='*.sqlite*' -e "$copy_rsh" "$copy_host:/mnt/storage1/apps/pds/" "$scratch/"
102drift=$(rsync -rnc --delete --exclude='*.sqlite*' --out-format='%n' -e "$copy_rsh" "$copy_host:/mnt/storage1/apps/pds/" "$scratch/")
103test -z "$drift" || { echo 'PDS source changed during copy; retry the import' >&2; exit 1; }
104
105if [ "$production" = false ]; then
106 ssh -p "$target_port" "$target_host" "NOMAD_TOKEN=\$(cat /var/lib/studio/nomad.token) nomad job stop -yes $instance"
107fi
108ssh -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
110rsync -a --delete -e "ssh -p $target_port" "$scratch/" "$target_host:$root/"
111owner=$(ssh -p "$target_port" "$target_host" "python3 -c 'import json; print(json.load(open(\"/var/lib/studio/identities.json\"))[\"pds\"])'")
112ssh -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)'"
113drift=$(rsync -rnc --delete --out-format='%n' -e "ssh -p $target_port" "$scratch/" "$target_host:$root/")
114test -z "$drift" || { echo "PDS copy differs from source backup; restore $snapshot" >&2; exit 1; }
115
116if [ "$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"
123else
124 python3 "$(dirname "$0")/deploy.py" stage pds
125 ssh -p "$target_port" "$target_host" "zfs destroy $snapshot"
126 echo "Imported and tested $instance"
127fi