| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | : "${STUDIO_DEPLOY_HOST:?Set STUDIO_DEPLOY_HOST to the production target}" |
| 5 | source_host=${STUDIO_MIGRATION_SOURCE:-zenith} |
| 6 | target_host=$STUDIO_DEPLOY_HOST |
| 7 | target_port=${STUDIO_DEPLOY_PORT:-22} |
| 8 | remote=(ssh -p "$target_port" "$target_host") |
| 9 | target_dir=/srv/prod/clover-source-of-truth/data |
| 10 | tree_hash="$(dirname "$0")/tree-hash.py" |
| 11 | |
| 12 | if [[ -n ${STUDIO_MIGRATION_SNAPSHOT:-} ]]; then |
| 13 | [[ $STUDIO_MIGRATION_SNAPSHOT =~ ^[A-Za-z0-9_-]+$ ]] || { echo 'Invalid source snapshot name' >&2; exit 1; } |
| 14 | source=("${remote[@]}") |
| 15 | source_dir="/srv/clover/.zfs/snapshot/$STUDIO_MIGRATION_SNAPSHOT/Documents/Config/paper clover" |
| 16 | "${source[@]}" "test -d '$source_dir'" |
| 17 | else |
| 18 | source=(ssh "$source_host") |
| 19 | source_dir='/mnt/storage1/clover/Documents/Config/paper clover' |
| 20 | [[ $("${source[@]}" "sudo -n docker inspect -f '{{.State.Running}}' clover-source-of-truth") == false ]] || { |
| 21 | echo 'Stop Zenith Source of Truth before importing production data' >&2 |
| 22 | exit 1 |
| 23 | } |
| 24 | fi |
| 25 | "${remote[@]}" "set -e; test \$(findmnt -n -o FSTYPE --mountpoint /srv/clover) = zfs; test -d /srv/clover/Published; ! findmnt -n -R '$target_dir' >/dev/null" |
| 26 | dataset=$("${remote[@]}" 'findmnt -n -o SOURCE --mountpoint /srv/prod/clover-source-of-truth') |
| 27 | [[ $dataset == */prod/clover-source-of-truth ]] || { echo 'Production service dataset is not mounted' >&2; exit 1; } |
| 28 | job_response=$("${remote[@]}" 'curl -s -w "\n%{http_code}" -H "X-Nomad-Token: $(cat /var/lib/studio/nomad.token)" http://127.0.0.1:4646/v1/job/clover-source-of-truth') |
| 29 | job_status=${job_response##*$'\n'} |
| 30 | if [[ $job_status == 200 ]]; then |
| 31 | stopped=$(python3 -c 'import json,sys; print(str(json.load(sys.stdin)["Stop"]).lower())' <<<"${job_response%$'\n'*}") |
| 32 | [[ $stopped == true ]] || { echo 'Stop Source of Truth on the home server before importing production data' >&2; exit 1; } |
| 33 | for _ in {1..30}; do |
| 34 | running=$("${remote[@]}" 'NOMAD_TOKEN=$(cat /var/lib/studio/nomad.token) nomad job allocs -json clover-source-of-truth' | |
| 35 | python3 -c 'import json,sys; print(sum(a["ClientStatus"] == "running" for a in json.load(sys.stdin)))') |
| 36 | [[ $running == 0 ]] && break |
| 37 | sleep 2 |
| 38 | done |
| 39 | [[ $running == 0 ]] || { echo 'The home server allocation did not stop' >&2; exit 1; } |
| 40 | elif [[ $job_status != 404 ]]; then |
| 41 | echo "Could not verify the home server job: $job_status" >&2 |
| 42 | exit 1 |
| 43 | fi |
| 44 | |
| 45 | source_manifest=$("${source[@]}" "python3 - '$source_dir'" < "$tree_hash") |
| 46 | source_bytes=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["bytes"])' <<<"$source_manifest") |
| 47 | available=$("${remote[@]}" "df -B1 --output=avail /srv/prod/clover-source-of-truth | tail -n 1") |
| 48 | (( available > source_bytes )) || { echo 'Production dataset has too little free space for the source tree' >&2; exit 1; } |
| 49 | |
| 50 | snapshot=$dataset@before-source-data-import-$(date +%s)-$$ |
| 51 | "${remote[@]}" "set -e; zfs snapshot $snapshot; rm -rf -- '$target_dir'; mkdir -p -- '$target_dir'" |
| 52 | if [[ -n ${STUDIO_MIGRATION_SNAPSHOT:-} ]]; then |
| 53 | "${remote[@]}" "bash -c \"set -euo pipefail; tar -C '$source_dir' -cf - . | tar -C '$target_dir' -xf -\"" |
| 54 | else |
| 55 | "${source[@]}" "tar -C '$source_dir' -cf - ." | |
| 56 | "${remote[@]}" "tar -C '$target_dir' -xf -" |
| 57 | fi |
| 58 | target_manifest=$("${remote[@]}" "python3 - '$target_dir'" < "$tree_hash") |
| 59 | [[ $source_manifest == "$target_manifest" ]] || { echo "Source and target trees differ; restore $snapshot" >&2; exit 1; } |
| 60 | uid=$("${remote[@]}" "python3 -c 'import json; print(json.load(open(\"/var/lib/studio/identities.json\"))[\"clover-source-of-truth\"])'") |
| 61 | "${remote[@]}" "set -e; chown -R $uid:$uid '$target_dir'; python3 -c 'import sqlite3; db=sqlite3.connect(\"file:$target_dir/cache.sqlite?mode=ro\", uri=True); assert db.execute(\"PRAGMA integrity_check\").fetchone()[0] == \"ok\"'" |
| 62 | if [[ -z ${STUDIO_MIGRATION_SNAPSHOT:-} ]]; then |
| 63 | [[ $("${source[@]}" "sudo -n docker inspect -f '{{.State.Running}}' clover-source-of-truth") == false ]] || { |
| 64 | echo 'Zenith Source of Truth restarted during import; leave the home server stopped' >&2 |
| 65 | exit 1 |
| 66 | } |
| 67 | fi |
| 68 | echo "Imported ${source_bytes} bytes without local staging. Previous dataset state: $snapshot" |