#!/usr/bin/env bash # # Copies one Site's data from the Docker deployment on monoco to the rootless # Podman deployment on emily. # # scripts/copy-site-data.sh hopsngrains # scripts/copy-site-data.sh hopsngrains --live # copy while the app keeps serving # # Run it from a machine that can ssh to both hosts (the ssh aliases monoco and # emily, overridable with MONOCO= and EMILY=). This is the one-off part of the # move: the playbook deploys the container, this hands it the content. # # By default it stops the old container first, so the copy is of quiesced data, # and starts it again afterwards — monoco keeps serving until DNS moves. Use # --live to skip the stop; the app writes its manifests atomically and its images # are immutable once written, so a live copy is usually clean, but an upload # landing mid-copy is missed. Nothing on monoco is removed either way. # # Two details are load-bearing: # # * monoco cannot ssh to emily, so this machine is the relay — the tar streams # through it and nothing large is written locally. # * sudo on monoco, because /var/lib/menu. belongs to uid 1000 (the # ubuntu account) while the ssh user there is 1001. The extraction runs as # tanshu on emily, which is the uid keep-id maps the app onto, so the files # land owned by the right user rather than by root or a subuid. set -euo pipefail usage() { cat <<'USAGE' usage: copy-site-data.sh [--live] inventory name from ansible/hosts: hopsngrains, greatbear, hngmohali --live copy without stopping the old container (not quiesced) Environment overrides: MONOCO=host ssh target holding the Docker deployment (default: monoco) EMILY=host ssh target holding the rootless Podman deployment (default: emily) SRC_ROOT=dir absolute data root on monoco (default: /var/lib) DEST_ROOT=dir absolute data root on emily (default: /podman-data, resolved over ssh — ~ and $HOME are not expanded for you) USAGE } SITE="${1:-}" if [[ -z "$SITE" || "$SITE" == "-h" || "$SITE" == "--help" ]]; then usage [[ -n "$SITE" ]] || exit 1 exit 0 fi shift LIVE=0 for argument in "$@"; do case "$argument" in --live) LIVE=1 ;; *) echo "unknown argument: $argument" >&2; usage >&2; exit 1 ;; esac done cd "$(dirname "${BASH_SOURCE[0]}")/.." # The repo is the source of truth for which Sites exist and what they answer. VARS="ansible/vars/$SITE.yml" if ! grep -qE "^$SITE[[:space:]]" ansible/hosts || [[ ! -f "$VARS" ]]; then echo "'$SITE' is not a Site in ansible/hosts" >&2 exit 1 fi TITLE="$(sed -n 's/^title:[[:space:]]*//p' "$VARS" | head -n 1)" HOST="menu.$TITLE" CONTAINER="menu-app.$HOST" MONOCO="${MONOCO:-monoco}" EMILY="${EMILY:-emily}" SRC_ROOT="${SRC_ROOT:-/var/lib}" for tool in ssh tar; do command -v "$tool" >/dev/null 2>&1 || { echo "$tool is required" >&2; exit 1; } done # Resolved rather than left as a literal ~ or $HOME, so every path below is # absolute and can be quoted safely on the remote side. if [[ -z "${DEST_ROOT:-}" ]]; then DEST_ROOT="$(ssh -n "$EMILY" 'printf %s "$HOME"')/podman-data" fi SRC="$SRC_ROOT/$HOST" DEST="$DEST_ROOT/menu-$SITE" echo "==> $SITE: $MONOCO:$SRC -> $EMILY:$DEST" # Start the old container again however this exits, so a failed copy never # leaves the live Site down. restart_needed=0 restart_old() { if [[ "$restart_needed" -eq 1 ]]; then echo "==> starting $CONTAINER again on $MONOCO" ssh -n "$MONOCO" docker start "$CONTAINER" >/dev/null || true restart_needed=0 fi } trap restart_old EXIT # -n on every monoco call: ssh would otherwise eat this script's stdin, and the # copy below is a pipeline whose input matters. if [[ "$LIVE" -eq 0 ]]; then echo "==> stopping $CONTAINER on $MONOCO" ssh -n "$MONOCO" docker stop "$CONTAINER" >/dev/null restart_needed=1 fi echo "==> copying" ssh -n "$MONOCO" "sudo -n tar -C '$SRC' -cf - ." \ | ssh "$EMILY" "mkdir -p '$DEST' && tar -C '$DEST' -xf -" restart_old echo "==> verifying" src_sum="$(ssh -n "$MONOCO" "sudo -n sh -c 'cd $SRC && find . -type f -exec sha256sum {} +' | sort | sha256sum" | awk '{print $1}')" dst_sum="$(ssh -n "$EMILY" "cd '$DEST' && find . -type f -exec sha256sum {} + | sort | sha256sum" | awk '{print $1}')" if [[ -z "$src_sum" || "$src_sum" != "$dst_sum" ]]; then echo "the copy does not match the source: $src_sum != $dst_sum" >&2 exit 1 fi user="$(id -un)" strays="$(ssh -n "$EMILY" "find '$DEST' ! -user '$user'" | head -n 5)" if [[ -n "$strays" ]]; then echo "the copy holds files $user does not own, so keep-id will not see them:" >&2 echo "$strays" >&2 exit 1 fi echo echo "ok ${dst_sum:0:12} $(ssh -n "$EMILY" "du -sb '$DEST'" | awk '{print $1}') bytes, all owned by $user" echo echo "next:" echo " ./deploy.sh # builds HEAD and deploys every Site" echo " SITE=$SITE ./deploy.sh # or just this one" echo " then check the Site on a Host: override before moving DNS to $EMILY"