#!/usr/bin/env bash # Live GOA: dry-run → backup → apt upgrade (bank/exchange/merchant) → podman commit. # Host: koopa (hernani, podman). No Freigabe-bypass — caller must have Freigabe. # # ./goa-live-backup-upgrade.sh dry-run # ./goa-live-backup-upgrade.sh backup # stamp only (no apt / no commit) # ./goa-live-backup-upgrade.sh apply # backup + apt + commit + smoke # ./goa-live-backup-upgrade.sh status # # State/log: ~/backups/goa/upgrade-runs// # Mirror: koopa-admin-log/scripts/taler-shared/ set -euo pipefail BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}" EXCHANGE_CTR="${EXCHANGE_CTR:-taler-hacktivism-exchange-ansible}" MERCHANT_CTR="${MERCHANT_CTR:-taler-hacktivism}" BANK_IMG_LIVE="${BANK_IMG_LIVE:-localhost/taler-hacktivism-banking:live}" EXCHANGE_IMG_LIVE="${EXCHANGE_IMG_LIVE:-localhost/taler-hacktivism-exchange-ansible:landing}" MERCHANT_IMG_LIVE="${MERCHANT_IMG_LIVE:-localhost/taler-hacktivism-live:landing}" STAMP="${STAMP:-$(date -u +%Y%m%dT%H%MZ)}" ROOT="${ROOT:-$HOME/backups/goa}" RUN_DIR="${RUN_DIR:-$ROOT/upgrade-runs/$STAMP}" MODE="${1:-dry-run}" mkdir -p "$RUN_DIR" LOG="$RUN_DIR/run.log" STATUS="$RUN_DIR/STATUS" exec > >(tee -a "$LOG") 2>&1 log() { printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"; } set_status() { printf 'STATUS=%s\nSTAMP=%s\nWHEN=%s\nMODE=%s\n' "$1" "$STAMP" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$MODE" >"$STATUS" shift || true for kv in "$@"; do printf '%s\n' "$kv" >>"$STATUS"; done ln -sfn "$STAMP" "$ROOT/upgrade-runs/current" 2>/dev/null || true ln -sfn "upgrade-runs/$STAMP" "$ROOT/upgrade-current-run" 2>/dev/null || true } pod_root() { local ctr="$1"; shift podman exec -u root "$ctr" bash -lc "$*" } dry_one() { local name="$1" ctr="$2" pkgs="$3" log "=== DRY $name ($ctr) ===" pod_root "$ctr" " set -e export DEBIAN_FRONTEND=noninteractive apt-get update -qq echo '--- installed ---' dpkg -l $pkgs 2>/dev/null | awk '/^ii/{print \$2,\$3}' || true echo '--- candidates (policy sample) ---' for p in $pkgs; do apt-cache policy \"\$p\" 2>/dev/null | awk -v p=\"\$p\" 'NR<=6{print p\": \"\$0}' done echo '--- simulate install ---' apt-get -s install -y $pkgs 2>&1 | egrep -i '^(Inst |Remv |Conf |The following packages)' || true " } cmd_dry_run() { set_status DRY_RUN_RUNNING log "RUN_DIR=$RUN_DIR" df -h "$HOME" | tee "$RUN_DIR/df.txt" podman ps --format '{{.Names}} {{.Status}} {{.Image}}' | grep -E 'taler-hacktivism|goa-regio' | tee "$RUN_DIR/ctrs.txt" || true dry_one bank "$BANK_CTR" "libeufin-bank libeufin-common" dry_one exchange "$EXCHANGE_CTR" "taler-exchange taler-exchange-database taler-exchange-offline libtalerexchange taler-terms-generator" dry_one merchant "$MERCHANT_CTR" "taler-merchant taler-merchant-webui libtalermerchant taler-terms-generator" cat >"$RUN_DIR/PLAN.txt" </dev/null || true cp -a /var/lib/libeufin-bank/. /tmp/goa-bak/var-lib-libeufin-bank/ 2>/dev/null || true runuser -u postgres -- psql -lAt > /tmp/goa-bak/pg-databases.txt || true runuser -u postgres -- pg_dump -Fc libeufinbank > /tmp/goa-bak/libeufinbank.dump || \ runuser -u postgres -- pg_dump -Fc libeufinbank > /tmp/goa-bak/libeufinbank.dump runuser -u postgres -- pg_dump -Fc libeufin > /tmp/goa-bak/libeufin.dump 2>/dev/null || true du -sh /tmp/goa-bak/* > /tmp/goa-bak/du.txt || true tar -C /tmp/goa-bak -czf /tmp/goa-bank-bak.tgz . ' podman cp "$BANK_CTR:/tmp/goa-bank-bak.tgz" "$dest/goa-bank-bak.tgz" pod_root "$BANK_CTR" 'rm -rf /tmp/goa-bak /tmp/goa-bank-bak.tgz' } backup_ctr_exchange() { local dest="$1/exchange" mkdir -p "$dest" pod_root "$EXCHANGE_CTR" ' set -e mkdir -p /tmp/goa-bak rm -rf /tmp/goa-bak/* mkdir -p /tmp/goa-bak/etc-taler-exchange /tmp/goa-bak/offline cp -a /etc/taler-exchange/. /tmp/goa-bak/etc-taler-exchange/ 2>/dev/null || true cp -a /var/lib/taler-exchange/offline/. /tmp/goa-bak/offline/ 2>/dev/null || true runuser -u postgres -- psql -lAt > /tmp/goa-bak/pg-databases.txt || true DB=$(runuser -u postgres -- psql -lAt | awk -F"|" "/taler/ {print \$1}" | head -1 | tr -d " ") [ -n "$DB" ] || DB=taler-exchange runuser -u postgres -- pg_dump -Fc "$DB" > /tmp/goa-bak/taler-exchange.dump tar -C /tmp/goa-bak -czf /tmp/goa-exchange-bak.tgz . ' podman cp "$EXCHANGE_CTR:/tmp/goa-exchange-bak.tgz" "$dest/goa-exchange-bak.tgz" pod_root "$EXCHANGE_CTR" 'rm -rf /tmp/goa-bak /tmp/goa-exchange-bak.tgz' } backup_ctr_merchant() { local dest="$1/merchant" mkdir -p "$dest" pod_root "$MERCHANT_CTR" ' set -e mkdir -p /tmp/goa-bak rm -rf /tmp/goa-bak/* mkdir -p /tmp/goa-bak/etc-taler /tmp/goa-bak/etc-taler-merchant cp -a /etc/taler/. /tmp/goa-bak/etc-taler/ 2>/dev/null || true cp -a /etc/taler-merchant/. /tmp/goa-bak/etc-taler-merchant/ 2>/dev/null || true runuser -u postgres -- psql -lAt > /tmp/goa-bak/pg-databases.txt || true DB=$(runuser -u postgres -- psql -lAt | awk -F"|" "/merchant|taler/ {print \$1}" | head -1 | tr -d " ") [ -n "$DB" ] || DB=taler-merchant runuser -u postgres -- pg_dump -Fc "$DB" > /tmp/goa-bak/taler-merchant.dump tar -C /tmp/goa-bak -czf /tmp/goa-merchant-bak.tgz . ' podman cp "$MERCHANT_CTR:/tmp/goa-merchant-bak.tgz" "$dest/goa-merchant-bak.tgz" pod_root "$MERCHANT_CTR" 'rm -rf /tmp/goa-bak /tmp/goa-merchant-bak.tgz' } cmd_backup() { local bak="$ROOT/$STAMP" mkdir -p "$bak/meta" log "BACKUP → $bak" { date -R hostname podman ps -a --format '{{.Names}} | {{.Image}} | {{.Status}}' } >"$bak/meta/inventory.txt" backup_ctr_bank "$bak" backup_ctr_exchange "$bak" backup_ctr_merchant "$bak" du -sh "$bak"/* >"$bak/meta/du.txt" || true find "$bak" -type f -printf '%s %p\n' >"$bak/meta/manifest-sizes.txt" || true ln -sfn "$STAMP" "$ROOT/current" log "BACKUP_OK current→$STAMP" echo "$bak" } upgrade_bank() { log "======== apt bank ========" pod_root "$BANK_CTR" ' set -e export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y libeufin-bank libeufin-common dpkg -l libeufin-bank libeufin-common | awk "/^ii/{print \$2,\$3}" ' pod_root "$BANK_CTR" ' set -e if ! runuser -u postgres -- psql -c "SELECT 1" >/dev/null 2>&1; then pg_ctlcluster 17 main start || true sleep 2 fi mkdir -p /var/log/libeufin-bank chown libeufin-bank:libeufin-bank /var/log/libeufin-bank 2>/dev/null || true runuser -u libeufin-bank -- /usr/local/bin/start_bank.sh --restart ' } upgrade_exchange() { log "======== apt exchange ========" pod_root "$EXCHANGE_CTR" ' set -e export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y \ taler-exchange taler-exchange-database taler-exchange-offline \ taler-exchange-typst libtalerexchange taler-terms-generator \ taler-exchange-aml-webui taler-exchange-kyc-webui || true dpkg -l "taler-exchange*" "libtalerexchange" "taler-terms-generator" 2>/dev/null \ | awk "/^ii/{print \$2,\$3}" ' pod_root "$EXCHANGE_CTR" ' set -e if id taler-exchange-httpd >/dev/null 2>&1; then runuser -u taler-exchange-httpd -- taler-exchange-dbinit 2>&1 | tail -20 || true fi if [ -x /root/start_base_services_for_taler_exchange.sh ]; then /root/start_base_services_for_taler_exchange.sh --no-shell 2>/dev/null || true fi if [ -x /usr/local/bin/start_exchange.sh ]; then runuser -u taler-exchange-httpd -- /usr/local/bin/start_exchange.sh --restart 2>&1 | tail -30 || true fi pgrep -a taler-exchange-httpd | head -3 || true ' } upgrade_merchant() { log "======== apt merchant ========" pod_root "$MERCHANT_CTR" ' set -e export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y \ taler-merchant taler-merchant-webui taler-merchant-typst \ libtalermerchant taler-terms-generator dpkg -l "taler-merchant*" "libtalermerchant" 2>/dev/null | awk "/^ii/{print \$2,\$3}" ' pod_root "$MERCHANT_CTR" ' set -e if id taler-merchant-httpd >/dev/null 2>&1; then runuser -u taler-merchant-httpd -- taler-merchant-dbinit 2>&1 | tail -20 || true fi runuser -u taler-merchant-httpd -- /usr/local/bin/start_merchant.sh --restart ' } commit_images() { log "======== podman commit ========" local tag="upgraded-$STAMP" podman commit "$BANK_CTR" "localhost/taler-hacktivism-banking:$tag" podman tag "localhost/taler-hacktivism-banking:$tag" "$BANK_IMG_LIVE" podman commit "$EXCHANGE_CTR" "localhost/taler-hacktivism-exchange-ansible:$tag" podman tag "localhost/taler-hacktivism-exchange-ansible:$tag" "$EXCHANGE_IMG_LIVE" podman commit "$MERCHANT_CTR" "localhost/taler-hacktivism-live:$tag" podman tag "localhost/taler-hacktivism-live:$tag" "$MERCHANT_IMG_LIVE" podman images --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.Size}}' \ | grep -E "upgraded-$STAMP|:live|:landing" | tee "$RUN_DIR/images-after.txt" || true } smoke() { # Return 0=ok / 1=fail. Do NOT echo ok into stdout for capture: # with exec>tee, sm=$(smoke) would swallow log lines and never equal "1". log "======== smoke ========" local ok=1 for pair in "bank:9012:http" "exchange:9011:http" "merchant:9010:https"; do IFS=: read -r name port sch <<<"$pair" code=$(curl -skS -m 10 -o /dev/null -w '%{http_code}' "${sch}://127.0.0.1:${port}/config" || echo 000) log "loopback $name :$port → $code" [ "$code" = "200" ] || ok=0 done for url in \ https://bank.hacktivism.ch/config \ https://exchange.hacktivism.ch/config \ https://taler.hacktivism.ch/config; do code=$(curl -skS -m 15 -o /dev/null -w '%{http_code}' "$url" || echo 000) log "public $url → $code" [ "$code" = "200" ] || ok=0 done [ "$ok" = "1" ] } versions_snap() { log "======== versions after ========" pod_root "$BANK_CTR" 'dpkg -l libeufin-bank libeufin-common | awk "/^ii/{print \$2,\$3}"' | tee "$RUN_DIR/ver-bank.txt" pod_root "$EXCHANGE_CTR" 'dpkg -l "taler-exchange" "libtalerexchange" 2>/dev/null | awk "/^ii/{print \$2,\$3}"' | tee "$RUN_DIR/ver-exchange.txt" pod_root "$MERCHANT_CTR" 'dpkg -l "taler-merchant" "taler-merchant-webui" "libtalermerchant" 2>/dev/null | awk "/^ii/{print \$2,\$3}"' | tee "$RUN_DIR/ver-merchant.txt" } cmd_apply() { set_status APPLY_RUNNING cmd_backup set_status APPLY_BACKUP_OK "BAK=$ROOT/$STAMP" upgrade_bank upgrade_exchange upgrade_merchant set_status APPLY_APT_OK commit_images set_status APPLY_COMMIT_OK versions_snap if smoke; then set_status APPLY_OK "NEXT=elelmon-moderate" "BAK=$ROOT/$STAMP" "LOG=$LOG" log "APPLY_OK — next: elelmon moderate from laptop" else set_status APPLY_SMOKE_FAIL "NEXT=triage-smoke" "BAK=$ROOT/$STAMP" "LOG=$LOG" log "APPLY_SMOKE_FAIL" exit 1 fi } cmd_status() { local cur="$ROOT/upgrade-runs/current" if [ -L "$cur" ] || [ -d "$cur" ]; then echo "CURRENT_RUN=$(readlink -f "$ROOT/upgrade-runs/current" 2>/dev/null || echo "$cur")" cat "$ROOT/upgrade-runs/current/STATUS" 2>/dev/null || cat "$cur/STATUS" 2>/dev/null || echo 'no STATUS' else echo 'NO_RUN' fi ls -la "$ROOT/upgrade-runs/" 2>/dev/null | tail -10 || true } case "$MODE" in dry-run|dry) cmd_dry_run ;; backup) set_status BACKUP_RUNNING cmd_backup set_status BACKUP_OK "STAMP=$STAMP" "CURRENT=$ROOT/current" log "BACKUP_OK" ;; apply) cmd_apply ;; status) cmd_status ;; -h|--help|help) sed -n '2,16p' "$0" exit 0 ;; *) echo "usage: $0 dry-run|backup|apply|status" >&2 exit 2 ;; esac